@uniai-fe/uds-templates 0.3.3 → 0.3.5
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.
- package/dist/styles.css +7 -5
- package/package.json +8 -7
- package/src/modal/components/alert/Contents.tsx +28 -0
- package/src/modal/components/alert/Template.tsx +59 -0
- package/src/modal/components/core/Body.tsx +23 -0
- package/src/modal/components/core/Container.tsx +59 -0
- package/src/modal/{core/components → components/core}/Root.tsx +12 -10
- package/src/modal/{core/components → components/core}/RouteReset.tsx +4 -1
- package/src/modal/{core/components/Provider.tsx → components/core/StackProvider.tsx} +12 -6
- package/src/modal/{core/components/FooterPositionButton.tsx → components/core/footer/Button.tsx} +13 -4
- package/src/modal/components/core/footer/ButtonGroup.tsx +34 -0
- package/src/modal/{core/components/FooterButtons.tsx → components/core/footer/ButtonWrapper.tsx} +28 -19
- package/src/modal/components/core/header/CloseButton.tsx +32 -0
- package/src/modal/components/core/header/Container.tsx +21 -0
- package/src/modal/components/dialog/Contents.tsx +32 -0
- package/src/modal/{templates/components/DialogHeader.tsx → components/dialog/Header.tsx} +17 -5
- package/src/modal/components/dialog/Template.tsx +86 -0
- package/src/modal/components/index.tsx +27 -0
- package/src/modal/{core/hooks → hooks}/useModal.ts +16 -13
- package/src/modal/index.tsx +12 -13
- package/src/modal/{core/jotai → jotai}/atoms.ts +2 -2
- package/src/modal/styles/container.scss +14 -5
- package/src/modal/types/components.ts +139 -4
- package/src/modal/types/index.ts +7 -1
- package/src/modal/utils/create-alert-footer-buttons.ts +59 -0
- package/src/modal/utils/create-dialog-footer-buttons.ts +72 -0
- package/src/page-frame/desktop/components/header/util/setting/Button.tsx +7 -11
- package/src/modal/core/components/Container.tsx +0 -55
- package/src/modal/core/components/FooterPositionGroup.tsx +0 -29
- package/src/modal/templates/Alert.tsx +0 -115
- package/src/modal/templates/Dialog.tsx +0 -127
- package/src/modal/templates/components/DialogBody.tsx +0 -20
package/dist/styles.css
CHANGED
|
@@ -606,7 +606,8 @@
|
|
|
606
606
|
word-break: keep-all;
|
|
607
607
|
color: var(--modal-alert-body-color);
|
|
608
608
|
}
|
|
609
|
-
.uds-modal-alert-message :where(p, span, strong, em) {
|
|
609
|
+
.uds-modal-alert-message > :where(p, span, strong, em):not([class]) {
|
|
610
|
+
margin: 0;
|
|
610
611
|
font-size: var(--modal-alert-body-font-size);
|
|
611
612
|
line-height: 1.5em;
|
|
612
613
|
font-weight: var(--font-body-small-weight, 400);
|
|
@@ -653,7 +654,7 @@
|
|
|
653
654
|
align-items: center;
|
|
654
655
|
gap: var(--spacing-gap-1, 4px);
|
|
655
656
|
}
|
|
656
|
-
.uds-modal-dialog-header-leading-content :where(p, span, strong, em) {
|
|
657
|
+
.uds-modal-dialog-header-leading-content > :where(p, span, strong, em):not([class]) {
|
|
657
658
|
color: var(--modal-dialog-title-color);
|
|
658
659
|
font-size: var(--modal-dialog-body-font-size);
|
|
659
660
|
line-height: 1.4em;
|
|
@@ -665,7 +666,7 @@
|
|
|
665
666
|
justify-content: center;
|
|
666
667
|
text-align: center;
|
|
667
668
|
}
|
|
668
|
-
.uds-modal-dialog-header-title > :where(h1, h2, h3, h4, h5, h6, p, span, strong, em) {
|
|
669
|
+
.uds-modal-dialog-header-title > :where(h1, h2, h3, h4, h5, h6, p, span, strong, em):not([class]) {
|
|
669
670
|
margin: 0;
|
|
670
671
|
color: var(--modal-dialog-title-color);
|
|
671
672
|
font-size: var(--modal-dialog-title-font-size);
|
|
@@ -683,7 +684,7 @@
|
|
|
683
684
|
margin: 0;
|
|
684
685
|
text-align: inherit;
|
|
685
686
|
}
|
|
686
|
-
.uds-modal-dialog-header-description > :where(p, span, strong, em) {
|
|
687
|
+
.uds-modal-dialog-header-description > :where(p, span, strong, em):not([class]) {
|
|
687
688
|
margin: 0;
|
|
688
689
|
color: var(--modal-dialog-body-color);
|
|
689
690
|
font-size: var(--modal-dialog-body-font-size);
|
|
@@ -707,7 +708,8 @@
|
|
|
707
708
|
word-break: keep-all;
|
|
708
709
|
color: var(--modal-dialog-body-color);
|
|
709
710
|
}
|
|
710
|
-
.uds-modal-dialog-body-content :where(p, span, strong, em) {
|
|
711
|
+
.uds-modal-dialog-body-content > :where(p, span, strong, em):not([class]) {
|
|
712
|
+
margin: 0;
|
|
711
713
|
font-size: var(--modal-dialog-body-font-size);
|
|
712
714
|
line-height: 1.5em;
|
|
713
715
|
font-weight: var(--font-body-small-weight, 400);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
|
-
"packageManager": "pnpm@10.
|
|
15
|
+
"packageManager": "pnpm@10.30.2",
|
|
16
16
|
"engines": {
|
|
17
17
|
"node": ">=24",
|
|
18
18
|
"pnpm": ">=10"
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"next": "^15",
|
|
62
62
|
"react": "^19",
|
|
63
63
|
"react-dom": "^19",
|
|
64
|
-
"react-hook-form": "^7"
|
|
64
|
+
"react-hook-form": "^7",
|
|
65
|
+
"clsx": "^2.1.1"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"clsx": "^2.1.1",
|
|
@@ -69,9 +70,9 @@
|
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@svgr/webpack": "^8.1.0",
|
|
72
|
-
"@tanstack/react-query": "^5.90.
|
|
73
|
+
"@tanstack/react-query": "^5.90.21",
|
|
73
74
|
"@types/node": "^24.10.2",
|
|
74
|
-
"@types/react": "^19.2.
|
|
75
|
+
"@types/react": "^19.2.14",
|
|
75
76
|
"@types/react-dom": "^19.2.3",
|
|
76
77
|
"@uniai-fe/eslint-config": "workspace:*",
|
|
77
78
|
"@uniai-fe/next-devkit": "workspace:*",
|
|
@@ -85,10 +86,10 @@
|
|
|
85
86
|
"@uniai-fe/util-next": "workspace:*",
|
|
86
87
|
"@uniai-fe/util-rtc": "workspace:*",
|
|
87
88
|
"eslint": "^9.39.2",
|
|
88
|
-
"jotai": "^2.
|
|
89
|
+
"jotai": "^2.18.0",
|
|
89
90
|
"next": "^15.5.11",
|
|
90
91
|
"prettier": "^3.8.1",
|
|
91
|
-
"react-hook-form": "^7.71.
|
|
92
|
+
"react-hook-form": "^7.71.2",
|
|
92
93
|
"sass": "^1.97.3",
|
|
93
94
|
"typescript": "~5.9.3"
|
|
94
95
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { AlertContentsProps } from "../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal Alert Contents; Alert 본문 메시지 콘텐츠
|
|
9
|
+
* @component
|
|
10
|
+
* @param {AlertContentsProps} props
|
|
11
|
+
* @param {React.ReactNode} props.message alert 본문 메시지
|
|
12
|
+
* @param {string} [props.className] 콘텐츠 wrapper className
|
|
13
|
+
* @example
|
|
14
|
+
* <AlertContents message="저장되었습니다." />
|
|
15
|
+
*/
|
|
16
|
+
export default function AlertContents({
|
|
17
|
+
message,
|
|
18
|
+
className,
|
|
19
|
+
}: AlertContentsProps) {
|
|
20
|
+
// 변경: text 전용 children은 string | number일 때만 래핑해 스타일 충돌을 방지한다.
|
|
21
|
+
const shouldWrapAsText = ["string", "number"].includes(typeof message);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={clsx("uds-modal-alert-message", className)}>
|
|
25
|
+
{shouldWrapAsText ? <p>{message}</p> : message}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import AlertContents from "./Contents";
|
|
4
|
+
import { createAlertFooterButtons } from "../../utils/create-alert-footer-buttons";
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AlertTemplateOptions,
|
|
8
|
+
ModalState,
|
|
9
|
+
ModalTemplateButtonSpec,
|
|
10
|
+
} from "../../types";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIRM_LABEL = "확인";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Modal Alert Template; Alert 모달 상태 생성기
|
|
16
|
+
* @component
|
|
17
|
+
* @param {AlertTemplateOptions} options Alert 모달 옵션
|
|
18
|
+
* @param {string} options.stackKey 모달 스택 키
|
|
19
|
+
* @param {React.ReactNode} options.message 본문 메시지
|
|
20
|
+
* @param {ModalTemplateButtonSpec} [options.confirm] 확인 버튼 스펙
|
|
21
|
+
* @param {ModalTemplateButtonSpec} [options.cancel] 취소 버튼 스펙
|
|
22
|
+
* @param {React.ReactNode} [options.footer] 완전 커스텀 footer
|
|
23
|
+
* @param {number} [options.showDelay] close 후 제거 지연(ms)
|
|
24
|
+
* @param {number | string} [options.width] 모달 width
|
|
25
|
+
* @param {StyleSpacingType} [options.padding] 모달 body padding
|
|
26
|
+
* @returns {ModalState}
|
|
27
|
+
* @example
|
|
28
|
+
* Modal.Alert({ stackKey: "sample", message: "완료되었습니다." })
|
|
29
|
+
*/
|
|
30
|
+
export function createAlertModal({
|
|
31
|
+
stackKey,
|
|
32
|
+
message,
|
|
33
|
+
confirm,
|
|
34
|
+
cancel,
|
|
35
|
+
footer,
|
|
36
|
+
showDelay,
|
|
37
|
+
width,
|
|
38
|
+
padding,
|
|
39
|
+
}: AlertTemplateOptions): ModalState {
|
|
40
|
+
const primary: ModalTemplateButtonSpec = confirm ?? {
|
|
41
|
+
label: DEFAULT_CONFIRM_LABEL,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
stackKey,
|
|
46
|
+
modalProps: {
|
|
47
|
+
show: "init",
|
|
48
|
+
showDelay,
|
|
49
|
+
width,
|
|
50
|
+
padding,
|
|
51
|
+
body: <AlertContents message={message} />,
|
|
52
|
+
footer,
|
|
53
|
+
// 변경: footer가 없을 때만 템플릿 버튼 스펙을 조합한다.
|
|
54
|
+
footerButtons: footer
|
|
55
|
+
? undefined
|
|
56
|
+
: createAlertFooterButtons({ stackKey, primary, cancel }),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { ModalBodyProps } from "../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal Body; 모달 body wrapper
|
|
9
|
+
* @component
|
|
10
|
+
* @param {ModalBodyProps} props
|
|
11
|
+
* @param {React.ReactNode} props.children body 내부 콘텐츠
|
|
12
|
+
* @param {React.CSSProperties} [props.style] 런타임 주입 스타일
|
|
13
|
+
* @param {string} [props.className] body className
|
|
14
|
+
* @example
|
|
15
|
+
* <ModalBody><div>본문</div></ModalBody>
|
|
16
|
+
*/
|
|
17
|
+
export function ModalBody({ children, style, className }: ModalBodyProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={clsx("uds-modal-body", className)} style={style}>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties } from "react";
|
|
4
|
+
|
|
5
|
+
import clsx from "clsx";
|
|
6
|
+
import { stylePaddingSize } from "@uniai-fe/util-functions";
|
|
7
|
+
|
|
8
|
+
import type { ModalContainerProps } from "../../types";
|
|
9
|
+
import { ModalBody } from "./Body";
|
|
10
|
+
import { ModalHeaderContainer } from "./header/Container";
|
|
11
|
+
import { ModalFooterButtonWrapper } from "./footer/ButtonWrapper";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Modal Container; header/body/footer 슬롯 조합 컨테이너
|
|
15
|
+
* @component
|
|
16
|
+
* @param {ModalContainerProps} props
|
|
17
|
+
* @param {string} props.stackKey 모달 스택 키
|
|
18
|
+
* @param {React.ReactNode} [props.header] 헤더 슬롯
|
|
19
|
+
* @param {React.ReactNode} props.body 본문 슬롯
|
|
20
|
+
* @param {React.ReactNode} [props.footer] footer 슬롯
|
|
21
|
+
* @param {ModalFooterButton[]} [props.footerButtons] footer 버튼 스펙 배열
|
|
22
|
+
* @param {StyleSpacingType} [props.padding] body padding 오버라이드
|
|
23
|
+
* @param {string} [props.className] 컨테이너 className
|
|
24
|
+
* @example
|
|
25
|
+
* <ModalContainer stackKey="sample" body={<div />} />
|
|
26
|
+
*/
|
|
27
|
+
export function ModalContainer({
|
|
28
|
+
stackKey,
|
|
29
|
+
header,
|
|
30
|
+
body,
|
|
31
|
+
footer,
|
|
32
|
+
footerButtons,
|
|
33
|
+
padding,
|
|
34
|
+
className,
|
|
35
|
+
}: ModalContainerProps) {
|
|
36
|
+
// 변경: modalProps의 padding을 CSS 변수로 주입해 템플릿 단위 body spacing 제어를 유지한다.
|
|
37
|
+
const bodyStyle =
|
|
38
|
+
typeof padding !== "undefined"
|
|
39
|
+
? ({
|
|
40
|
+
"--modal-dialog-body-padding": stylePaddingSize("rem", padding),
|
|
41
|
+
} as CSSProperties)
|
|
42
|
+
: undefined;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className={clsx("uds-modal-container", className)}>
|
|
46
|
+
{header ? <ModalHeaderContainer>{header}</ModalHeaderContainer> : null}
|
|
47
|
+
<ModalBody style={bodyStyle}>{body}</ModalBody>
|
|
48
|
+
{footer ? <div className="uds-modal-footer">{footer}</div> : null}
|
|
49
|
+
{!footer && footerButtons && footerButtons.length ? (
|
|
50
|
+
<div className="uds-modal-footer">
|
|
51
|
+
<ModalFooterButtonWrapper
|
|
52
|
+
stackKey={stackKey}
|
|
53
|
+
buttons={footerButtons}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -2,25 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo } from "react";
|
|
4
4
|
import type { CSSProperties } from "react";
|
|
5
|
-
import clsx from "clsx";
|
|
6
5
|
|
|
7
|
-
import
|
|
8
|
-
import { ModalContainer } from "./Container";
|
|
6
|
+
import clsx from "clsx";
|
|
9
7
|
|
|
10
8
|
import type { ModalRootProps } from "../../types";
|
|
9
|
+
import { useModal } from "../../hooks/useModal";
|
|
10
|
+
import { ModalContainer } from "./Container";
|
|
11
11
|
|
|
12
12
|
type ModalSurfaceStyle = CSSProperties & {
|
|
13
13
|
"--modal-panel-width"?: string | number;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Modal Root; overlay 클릭 닫기와 상태 전환 처리
|
|
18
18
|
* @component
|
|
19
|
-
* @param {ModalRootProps} props
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
23
|
-
* @
|
|
19
|
+
* @param {ModalRootProps} props
|
|
20
|
+
* @param {string} props.stackKey 모달 스택 식별자
|
|
21
|
+
* @param {ModalProps} props.modalProps 모달 렌더링에 필요한 상태
|
|
22
|
+
* @param {number} props.index Provider에서 전달한 스택 index
|
|
23
|
+
* @param {string} [props.className] 사용자 정의 className
|
|
24
|
+
* @example
|
|
25
|
+
* <ModalRoot stackKey="sample" modalProps={modalProps} index={0} />
|
|
24
26
|
*/
|
|
25
27
|
export function ModalRoot({
|
|
26
28
|
stackKey,
|
|
@@ -47,7 +49,7 @@ export function ModalRoot({
|
|
|
47
49
|
[],
|
|
48
50
|
);
|
|
49
51
|
|
|
50
|
-
// show 상태를 data-state로
|
|
52
|
+
// 변경: show 상태를 data-state로 매핑해 애니메이션 상태를 명확히 유지한다.
|
|
51
53
|
const dataState = useMemo(() => {
|
|
52
54
|
if (modalProps.show === "init") {
|
|
53
55
|
return "init";
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "react";
|
|
4
|
+
|
|
4
5
|
import { usePathname } from "next/navigation";
|
|
5
6
|
import { useSetAtom } from "jotai";
|
|
6
7
|
|
|
7
|
-
import { modalStackAtom } from "
|
|
8
|
+
import { modalStackAtom } from "../../jotai/atoms";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Modal Route Reset; 라우트 변경 시 모달 스택 초기화
|
|
@@ -12,6 +13,8 @@ import { modalStackAtom } from "../jotai/atoms";
|
|
|
12
13
|
* @desc
|
|
13
14
|
* - Next.js pathname 변경을 감지해 `modalStackAtom`을 빈 배열로 되돌린다.
|
|
14
15
|
* - 최상위 layout에서 `<Modal.RouteReset />`을 Provider와 함께 렌더링해야 한다.
|
|
16
|
+
* @example
|
|
17
|
+
* <ModalRouteReset />
|
|
15
18
|
*/
|
|
16
19
|
export function ModalRouteReset() {
|
|
17
20
|
const pathname = usePathname();
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import { Fragment } from "react";
|
|
4
4
|
|
|
5
|
-
import { useModal } from "
|
|
5
|
+
import { useModal } from "../../hooks/useModal";
|
|
6
6
|
import { ModalRoot } from "./Root";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Modal Stack Provider; 모달 스택 루트 렌더러
|
|
10
10
|
* @component
|
|
11
11
|
* @desc
|
|
12
|
-
* - `useModal()`에서 관리하는 스택을 순회하며 `ModalRoot`를
|
|
13
|
-
* -
|
|
12
|
+
* - `useModal()`에서 관리하는 스택을 순회하며 `ModalRoot`를 렌더링한다.
|
|
13
|
+
* - 스택이 비어 있으면 아무 것도 렌더하지 않는다.
|
|
14
|
+
* @example
|
|
15
|
+
* <ModalStackProvider />
|
|
14
16
|
*/
|
|
15
|
-
export function
|
|
17
|
+
export function ModalStackProvider() {
|
|
16
18
|
const { modalStacks } = useModal();
|
|
17
19
|
|
|
18
|
-
if (!modalStacks.length)
|
|
20
|
+
if (!modalStacks.length) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
return (
|
|
21
25
|
<Fragment>
|
|
@@ -29,3 +33,5 @@ export function ModalProvider() {
|
|
|
29
33
|
</Fragment>
|
|
30
34
|
);
|
|
31
35
|
}
|
|
36
|
+
|
|
37
|
+
export const ModalProvider = ModalStackProvider;
|
package/src/modal/{core/components/FooterPositionButton.tsx → components/core/footer/Button.tsx}
RENAMED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
import clsx from "clsx";
|
|
4
4
|
import { Button } from "@uniai-fe/uds-primitives";
|
|
5
5
|
|
|
6
|
-
import type { FooterPositionButtonProps } from "
|
|
6
|
+
import type { FooterPositionButtonProps } from "../../../types";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* footer 버튼
|
|
9
|
+
* Modal Footer Button; footer 단일 버튼 요소
|
|
10
10
|
* @component
|
|
11
|
-
* @param {FooterPositionButtonProps} props
|
|
11
|
+
* @param {FooterPositionButtonProps} props
|
|
12
12
|
* @param {FooterResolvedButton} props.button 렌더링할 버튼
|
|
13
|
+
* @param {string} [props.className] 버튼 className
|
|
14
|
+
* @example
|
|
15
|
+
* <ModalFooterButtonElement button={resolvedButton} />
|
|
13
16
|
*/
|
|
14
|
-
export function
|
|
17
|
+
export function ModalFooterButtonElement({
|
|
18
|
+
button,
|
|
19
|
+
className,
|
|
20
|
+
}: FooterPositionButtonProps) {
|
|
15
21
|
const { appearance, width, block, defaultOptions, linkOptions, onClick } =
|
|
16
22
|
button;
|
|
17
23
|
const anchorProps = linkOptions
|
|
@@ -28,6 +34,7 @@ export function FooterPositionButton({ button }: FooterPositionButtonProps) {
|
|
|
28
34
|
? "uds-modal-footer-button-text"
|
|
29
35
|
: "uds-modal-footer-button-solid",
|
|
30
36
|
defaultOptions.className,
|
|
37
|
+
className,
|
|
31
38
|
);
|
|
32
39
|
const commonProps = {
|
|
33
40
|
className: baseClassName,
|
|
@@ -43,6 +50,7 @@ export function FooterPositionButton({ button }: FooterPositionButtonProps) {
|
|
|
43
50
|
defaultOptions.priority && defaultOptions.priority !== "primary"
|
|
44
51
|
? defaultOptions.priority
|
|
45
52
|
: "secondary";
|
|
53
|
+
|
|
46
54
|
return (
|
|
47
55
|
<Button.Text
|
|
48
56
|
{...commonProps}
|
|
@@ -56,6 +64,7 @@ export function FooterPositionButton({ button }: FooterPositionButtonProps) {
|
|
|
56
64
|
|
|
57
65
|
const fill = defaultOptions.fill ?? "solid";
|
|
58
66
|
const size = defaultOptions.size ?? "large";
|
|
67
|
+
|
|
59
68
|
return (
|
|
60
69
|
<Button.Default
|
|
61
70
|
{...commonProps}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { FooterPositionGroupProps } from "../../../types";
|
|
6
|
+
import { ModalFooterButtonElement } from "./Button";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Modal Footer ButtonGroup; footer 위치 그룹 컨테이너
|
|
10
|
+
* @component
|
|
11
|
+
* @param {FooterPositionGroupProps} props
|
|
12
|
+
* @param {FooterPosition} props.position 렌더링 위치
|
|
13
|
+
* @param {FooterResolvedButton[]} props.buttons 위치별 버튼 목록
|
|
14
|
+
* @param {string} [props.className] 그룹 className
|
|
15
|
+
* @example
|
|
16
|
+
* <ModalFooterButtonGroup position="left" buttons={buttons} />
|
|
17
|
+
*/
|
|
18
|
+
export function ModalFooterButtonGroup({
|
|
19
|
+
position,
|
|
20
|
+
buttons,
|
|
21
|
+
className,
|
|
22
|
+
}: FooterPositionGroupProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={clsx("uds-modal-footer-group", className)}
|
|
26
|
+
data-position={position}
|
|
27
|
+
data-has-buttons={buttons.length > 0}
|
|
28
|
+
>
|
|
29
|
+
{buttons.map(button => (
|
|
30
|
+
<ModalFooterButtonElement key={button.elementKey} button={button} />
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
package/src/modal/{core/components/FooterButtons.tsx → components/core/footer/ButtonWrapper.tsx}
RENAMED
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { FooterPositionGroup } from "./FooterPositionGroup";
|
|
5
|
-
import { FooterPositionButton } from "./FooterPositionButton";
|
|
3
|
+
import clsx from "clsx";
|
|
6
4
|
|
|
7
5
|
import type {
|
|
8
|
-
FooterResolvedButton,
|
|
9
6
|
FooterPosition,
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
FooterResolvedButton,
|
|
8
|
+
FooterButtonWrapperProps,
|
|
9
|
+
} from "../../../types";
|
|
10
|
+
import { useModal } from "../../../hooks/useModal";
|
|
11
|
+
import { ModalFooterButtonElement } from "./Button";
|
|
12
|
+
import { ModalFooterButtonGroup } from "./ButtonGroup";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Modal Footer ButtonWrapper; footer 버튼 목록 조합 렌더러
|
|
15
16
|
* @component
|
|
16
|
-
* @param {
|
|
17
|
+
* @param {FooterButtonWrapperProps} props
|
|
17
18
|
* @param {string} props.stackKey 버튼이 속한 모달 스택 키
|
|
18
19
|
* @param {ModalFooterButton[]} props.buttons footer 버튼 정의 목록
|
|
20
|
+
* @param {string} [props.className] wrapper className
|
|
21
|
+
* @example
|
|
22
|
+
* <ModalFooterButtonWrapper stackKey="sample" buttons={buttons} />
|
|
19
23
|
*/
|
|
20
|
-
export function
|
|
24
|
+
export function ModalFooterButtonWrapper({
|
|
21
25
|
stackKey,
|
|
22
26
|
buttons,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
buttons: ModalFooterButton[];
|
|
26
|
-
}) {
|
|
27
|
+
className,
|
|
28
|
+
}: FooterButtonWrapperProps) {
|
|
27
29
|
const { closeModal } = useModal();
|
|
28
30
|
const resolvedButtons: FooterResolvedButton[] = buttons.map(
|
|
29
31
|
(button, index) => {
|
|
30
32
|
const appearance = button.defaultOptions.appearance ?? "default";
|
|
31
33
|
const position = button.position ?? "center";
|
|
32
|
-
// 기본 폭
|
|
34
|
+
// 변경: 기본 폭 정책은 center=fill, 좌/우=fit을 유지한다.
|
|
33
35
|
const width = button.width ?? (position === "center" ? "fill" : "fit");
|
|
34
36
|
const block =
|
|
35
37
|
typeof button.defaultOptions.block === "boolean"
|
|
@@ -37,12 +39,14 @@ export function ModalFooterButtons({
|
|
|
37
39
|
: width === "full";
|
|
38
40
|
const closeOnClick = button.closeOnClick ?? button.role === "close";
|
|
39
41
|
const elementKey = `modal/${stackKey}/footer/${button.role}/${index}`;
|
|
42
|
+
|
|
40
43
|
const handleClick = () => {
|
|
41
44
|
button.defaultOptions.onClick?.();
|
|
42
45
|
if (closeOnClick) {
|
|
43
46
|
closeModal({ stackKey });
|
|
44
47
|
}
|
|
45
48
|
};
|
|
49
|
+
|
|
46
50
|
return {
|
|
47
51
|
elementKey,
|
|
48
52
|
appearance,
|
|
@@ -55,6 +59,7 @@ export function ModalFooterButtons({
|
|
|
55
59
|
};
|
|
56
60
|
},
|
|
57
61
|
);
|
|
62
|
+
|
|
58
63
|
const groupAppearance = resolvedButtons.every(
|
|
59
64
|
button => button.appearance === "text",
|
|
60
65
|
)
|
|
@@ -63,18 +68,19 @@ export function ModalFooterButtons({
|
|
|
63
68
|
|
|
64
69
|
return (
|
|
65
70
|
<div
|
|
66
|
-
className="uds-modal-footer-buttons"
|
|
71
|
+
className={clsx("uds-modal-footer-buttons", className)}
|
|
67
72
|
data-count={resolvedButtons.length}
|
|
68
73
|
data-appearance={groupAppearance}
|
|
69
74
|
>
|
|
70
|
-
{/* start/end 그룹을 렌더링하고 center 버튼은 wrap 컨테이너로 처리한다. */}
|
|
71
75
|
{(["left", "right"] as FooterPosition[]).map(position => {
|
|
72
76
|
const groupButtons = resolvedButtons.filter(
|
|
73
77
|
button => button.position === position,
|
|
74
78
|
);
|
|
75
|
-
if (!groupButtons.length)
|
|
79
|
+
if (!groupButtons.length) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
76
82
|
return (
|
|
77
|
-
<
|
|
83
|
+
<ModalFooterButtonGroup
|
|
78
84
|
key={`modal/${stackKey}/footer-group/${position}`}
|
|
79
85
|
position={position}
|
|
80
86
|
buttons={groupButtons}
|
|
@@ -86,7 +92,10 @@ export function ModalFooterButtons({
|
|
|
86
92
|
{resolvedButtons
|
|
87
93
|
.filter(button => button.position === "center")
|
|
88
94
|
.map(button => (
|
|
89
|
-
<
|
|
95
|
+
<ModalFooterButtonElement
|
|
96
|
+
key={button.elementKey}
|
|
97
|
+
button={button}
|
|
98
|
+
/>
|
|
90
99
|
))}
|
|
91
100
|
</div>
|
|
92
101
|
) : null}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { ModalHeaderCloseButtonProps } from "../../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal Header CloseButton; 모달 헤더 닫기 버튼
|
|
9
|
+
* @component
|
|
10
|
+
* @param {ModalHeaderCloseButtonProps} props
|
|
11
|
+
* @param {() => void} props.onClick 클릭 핸들러
|
|
12
|
+
* @param {string} [props.ariaLabel] 접근성 라벨
|
|
13
|
+
* @param {string} [props.className] 버튼 className
|
|
14
|
+
* @example
|
|
15
|
+
* <ModalHeaderCloseButton onClick={handleClose} />
|
|
16
|
+
*/
|
|
17
|
+
export function ModalHeaderCloseButton({
|
|
18
|
+
onClick,
|
|
19
|
+
ariaLabel = "닫기",
|
|
20
|
+
className,
|
|
21
|
+
}: ModalHeaderCloseButtonProps) {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className={clsx("uds-modal-header-close-button", className)}
|
|
26
|
+
aria-label={ariaLabel}
|
|
27
|
+
onClick={onClick}
|
|
28
|
+
>
|
|
29
|
+
X
|
|
30
|
+
</button>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { ModalHeaderContainerProps } from "../../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal Header Container; 모달 header wrapper
|
|
9
|
+
* @component
|
|
10
|
+
* @param {ModalHeaderContainerProps} props
|
|
11
|
+
* @param {React.ReactNode} props.children 헤더 콘텐츠
|
|
12
|
+
* @param {string} [props.className] 헤더 wrapper className
|
|
13
|
+
* @example
|
|
14
|
+
* <ModalHeaderContainer><h3>제목</h3></ModalHeaderContainer>
|
|
15
|
+
*/
|
|
16
|
+
export function ModalHeaderContainer({
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
}: ModalHeaderContainerProps) {
|
|
20
|
+
return <div className={clsx("uds-modal-header", className)}>{children}</div>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { DialogContentsProps } from "../../types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Modal Dialog Contents; Dialog 본문 콘텐츠 영역
|
|
9
|
+
* @component
|
|
10
|
+
* @param {DialogContentsProps} props
|
|
11
|
+
* @param {React.ReactNode} props.content 본문 콘텐츠
|
|
12
|
+
* @param {string} [props.className] body wrapper className
|
|
13
|
+
* @param {string} [props.contentClassName] 본문 콘텐츠 className
|
|
14
|
+
* @example
|
|
15
|
+
* <DialogContents content="본문" />
|
|
16
|
+
*/
|
|
17
|
+
export function DialogContents({
|
|
18
|
+
content,
|
|
19
|
+
className,
|
|
20
|
+
contentClassName,
|
|
21
|
+
}: DialogContentsProps) {
|
|
22
|
+
// 변경: text 전용 children은 string | number일 때만 래핑해 스타일 충돌을 줄인다.
|
|
23
|
+
const shouldWrapAsText = ["string", "number"].includes(typeof content);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={clsx("uds-modal-dialog-body", className)}>
|
|
27
|
+
<div className={clsx("uds-modal-dialog-body-content", contentClassName)}>
|
|
28
|
+
{shouldWrapAsText ? <p>{content}</p> : content}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|