@uniai-fe/uds-templates 0.3.2 → 0.3.4

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 (32) hide show
  1. package/dist/styles.css +7 -5
  2. package/package.json +10 -7
  3. package/src/modal/components/alert/Contents.tsx +28 -0
  4. package/src/modal/components/alert/Template.tsx +59 -0
  5. package/src/modal/components/core/Body.tsx +23 -0
  6. package/src/modal/components/core/Container.tsx +59 -0
  7. package/src/modal/{core/components → components/core}/Root.tsx +12 -10
  8. package/src/modal/{core/components → components/core}/RouteReset.tsx +4 -1
  9. package/src/modal/{core/components/Provider.tsx → components/core/StackProvider.tsx} +12 -6
  10. package/src/modal/{core/components/FooterPositionButton.tsx → components/core/footer/Button.tsx} +13 -4
  11. package/src/modal/components/core/footer/ButtonGroup.tsx +34 -0
  12. package/src/modal/{core/components/FooterButtons.tsx → components/core/footer/ButtonWrapper.tsx} +28 -19
  13. package/src/modal/components/core/header/CloseButton.tsx +32 -0
  14. package/src/modal/components/core/header/Container.tsx +21 -0
  15. package/src/modal/components/dialog/Contents.tsx +32 -0
  16. package/src/modal/{templates/components/DialogHeader.tsx → components/dialog/Header.tsx} +17 -5
  17. package/src/modal/components/dialog/Template.tsx +86 -0
  18. package/src/modal/components/index.tsx +27 -0
  19. package/src/modal/{core/hooks → hooks}/useModal.ts +16 -13
  20. package/src/modal/index.tsx +12 -13
  21. package/src/modal/{core/jotai → jotai}/atoms.ts +2 -2
  22. package/src/modal/styles/container.scss +14 -5
  23. package/src/modal/types/components.ts +139 -4
  24. package/src/modal/types/index.ts +7 -1
  25. package/src/modal/utils/create-alert-footer-buttons.ts +59 -0
  26. package/src/modal/utils/create-dialog-footer-buttons.ts +72 -0
  27. package/src/types/api.ts +31 -57
  28. package/src/modal/core/components/Container.tsx +0 -55
  29. package/src/modal/core/components/FooterPositionGroup.tsx +0 -29
  30. package/src/modal/templates/Alert.tsx +0 -115
  31. package/src/modal/templates/Dialog.tsx +0 -127
  32. 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.2",
3
+ "version": "0.3.4",
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.28.2",
15
+ "packageManager": "pnpm@10.30.2",
16
16
  "engines": {
17
17
  "node": ">=24",
18
18
  "pnpm": ">=10"
@@ -52,6 +52,7 @@
52
52
  "@tanstack/react-query": "^5",
53
53
  "@uniai-fe/uds-foundation": "^0.1.0",
54
54
  "@uniai-fe/uds-primitives": "^0.1.0",
55
+ "@uniai-fe/util-api": "^0.1.0",
55
56
  "@uniai-fe/util-functions": "^0.2.0",
56
57
  "@uniai-fe/util-jotai": "^0.1.5",
57
58
  "@uniai-fe/util-next": "^0.2.0",
@@ -60,7 +61,8 @@
60
61
  "next": "^15",
61
62
  "react": "^19",
62
63
  "react-dom": "^19",
63
- "react-hook-form": "^7"
64
+ "react-hook-form": "^7",
65
+ "clsx": "^2.1.1"
64
66
  },
65
67
  "dependencies": {
66
68
  "clsx": "^2.1.1",
@@ -68,9 +70,9 @@
68
70
  },
69
71
  "devDependencies": {
70
72
  "@svgr/webpack": "^8.1.0",
71
- "@tanstack/react-query": "^5.90.20",
73
+ "@tanstack/react-query": "^5.90.21",
72
74
  "@types/node": "^24.10.2",
73
- "@types/react": "^19.2.11",
75
+ "@types/react": "^19.2.14",
74
76
  "@types/react-dom": "^19.2.3",
75
77
  "@uniai-fe/eslint-config": "workspace:*",
76
78
  "@uniai-fe/next-devkit": "workspace:*",
@@ -78,15 +80,16 @@
78
80
  "@uniai-fe/tsconfig": "workspace:*",
79
81
  "@uniai-fe/uds-foundation": "workspace:*",
80
82
  "@uniai-fe/uds-primitives": "workspace:*",
83
+ "@uniai-fe/util-api": "workspace:*",
81
84
  "@uniai-fe/util-functions": "workspace:*",
82
85
  "@uniai-fe/util-jotai": "workspace:*",
83
86
  "@uniai-fe/util-next": "workspace:*",
84
87
  "@uniai-fe/util-rtc": "workspace:*",
85
88
  "eslint": "^9.39.2",
86
- "jotai": "^2.17.1",
89
+ "jotai": "^2.18.0",
87
90
  "next": "^15.5.11",
88
91
  "prettier": "^3.8.1",
89
- "react-hook-form": "^7.71.1",
92
+ "react-hook-form": "^7.71.2",
90
93
  "sass": "^1.97.3",
91
94
  "typescript": "~5.9.3"
92
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 { useModal } from "../hooks/useModal";
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
- * 모달 Root; overlay 클릭 닫기와 상태 전환을 담당한다.
17
+ * Modal Root; overlay 클릭 닫기와 상태 전환 처리
18
18
  * @component
19
- * @param {ModalRootProps} props 루트 속성
20
- * @property {string} stackKey 모달 스택 식별자
21
- * @property {ModalProps} modalProps 모달 렌더링에 필요한 상태
22
- * @property {number} index Provider에서 전달한 스택 index
23
- * @property {string} [className] 사용자 정의 className
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 "../jotai/atoms";
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 "../hooks/useModal";
5
+ import { useModal } from "../../hooks/useModal";
6
6
  import { ModalRoot } from "./Root";
7
7
 
8
8
  /**
9
- * 서비스 루트에서 1회 배치되는 모달 Provider.
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 ModalProvider() {
17
+ export function ModalStackProvider() {
16
18
  const { modalStacks } = useModal();
17
19
 
18
- if (!modalStacks.length) return null;
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;
@@ -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 "../../types";
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 FooterPositionButton({ button }: FooterPositionButtonProps) {
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
+ }
@@ -1,35 +1,37 @@
1
1
  "use client";
2
2
 
3
- import { useModal } from "../hooks/useModal";
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
- ModalFooterButton,
11
- } from "../../types";
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
- * 모달 footer 버튼 리스트 렌더러.
15
+ * Modal Footer ButtonWrapper; footer 버튼 목록 조합 렌더러
15
16
  * @component
16
- * @param {object} props footer 버튼 렌더 옵션
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 ModalFooterButtons({
24
+ export function ModalFooterButtonWrapper({
21
25
  stackKey,
22
26
  buttons,
23
- }: {
24
- stackKey: string;
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
- // 기본 폭 정책: centerfill, 좌/우는 fit
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) return null;
79
+ if (!groupButtons.length) {
80
+ return null;
81
+ }
76
82
  return (
77
- <FooterPositionGroup
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
- <FooterPositionButton key={button.elementKey} button={button} />
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
+ }