@uniai-fe/uds-templates 0.3.4 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-templates",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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.30.2",
15
+ "packageManager": "pnpm@10.30.3",
16
16
  "engines": {
17
17
  "node": ">=24",
18
18
  "pnpm": ">=10"
@@ -8,6 +8,7 @@ import type {
8
8
  ModalState,
9
9
  ModalTemplateButtonSpec,
10
10
  } from "../../types";
11
+ import type { FieldValues } from "react-hook-form";
11
12
 
12
13
  const DEFAULT_CONFIRM_LABEL = "확인";
13
14
 
@@ -23,11 +24,12 @@ const DEFAULT_CONFIRM_LABEL = "확인";
23
24
  * @param {number} [options.showDelay] close 후 제거 지연(ms)
24
25
  * @param {number | string} [options.width] 모달 width
25
26
  * @param {StyleSpacingType} [options.padding] 모달 body padding
27
+ * @param {UseFormProps<FormContext>} [options.formContextOptions] modal 내부 Form.Provider 옵션
26
28
  * @returns {ModalState}
27
29
  * @example
28
30
  * Modal.Alert({ stackKey: "sample", message: "완료되었습니다." })
29
31
  */
30
- export function createAlertModal({
32
+ export function createAlertModal<FormContext extends FieldValues>({
31
33
  stackKey,
32
34
  message,
33
35
  confirm,
@@ -36,7 +38,8 @@ export function createAlertModal({
36
38
  showDelay,
37
39
  width,
38
40
  padding,
39
- }: AlertTemplateOptions): ModalState {
41
+ formContextOptions,
42
+ }: AlertTemplateOptions<FormContext>): ModalState<FormContext> {
40
43
  const primary: ModalTemplateButtonSpec = confirm ?? {
41
44
  label: DEFAULT_CONFIRM_LABEL,
42
45
  };
@@ -48,6 +51,8 @@ export function createAlertModal({
48
51
  showDelay,
49
52
  width,
50
53
  padding,
54
+ // 변경 설명: 템플릿 생성 시점의 FormContext 제네릭을 modalProps로 전달한다.
55
+ formContextOptions,
51
56
  body: <AlertContents message={message} />,
52
57
  footer,
53
58
  // 변경: footer가 없을 때만 템플릿 버튼 스펙을 조합한다.
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import type { CSSProperties } from "react";
3
+ import { type CSSProperties } from "react";
4
4
 
5
5
  import clsx from "clsx";
6
6
  import { stylePaddingSize } from "@uniai-fe/util-functions";
@@ -9,6 +9,7 @@ import type { ModalContainerProps } from "../../types";
9
9
  import { ModalBody } from "./Body";
10
10
  import { ModalHeaderContainer } from "./header/Container";
11
11
  import { ModalFooterButtonWrapper } from "./footer/ButtonWrapper";
12
+ import { Form } from "@uniai-fe/uds-primitives";
12
13
 
13
14
  /**
14
15
  * Modal Container; header/body/footer 슬롯 조합 컨테이너
@@ -32,6 +33,7 @@ export function ModalContainer({
32
33
  footerButtons,
33
34
  padding,
34
35
  className,
36
+ formContextOptions,
35
37
  }: ModalContainerProps) {
36
38
  // 변경: modalProps의 padding을 CSS 변수로 주입해 템플릿 단위 body spacing 제어를 유지한다.
37
39
  const bodyStyle =
@@ -41,19 +43,28 @@ export function ModalContainer({
41
43
  } as CSSProperties)
42
44
  : undefined;
43
45
 
46
+ const ContextProvider = ({ children }: { children: React.ReactNode }) =>
47
+ typeof formContextOptions !== "undefined" ? (
48
+ <Form.Provider options={formContextOptions}>{children}</Form.Provider>
49
+ ) : (
50
+ children
51
+ );
52
+
44
53
  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>
54
+ <ContextProvider>
55
+ <div className={clsx("uds-modal-container", className)}>
56
+ {header ? <ModalHeaderContainer>{header}</ModalHeaderContainer> : null}
57
+ <ModalBody style={bodyStyle}>{body}</ModalBody>
58
+ {footer ? <div className="uds-modal-footer">{footer}</div> : null}
59
+ {!footer && footerButtons && footerButtons.length ? (
60
+ <div className="uds-modal-footer">
61
+ <ModalFooterButtonWrapper
62
+ stackKey={stackKey}
63
+ buttons={footerButtons}
64
+ />
65
+ </div>
66
+ ) : null}
67
+ </div>
68
+ </ContextProvider>
58
69
  );
59
70
  }
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../utils/create-dialog-footer-buttons";
9
9
 
10
10
  import type { DialogTemplateOptions, ModalState } from "../../types";
11
+ import type { FieldValues } from "react-hook-form";
11
12
 
12
13
  /**
13
14
  * Modal Dialog Template; Dialog 모달 상태 생성기
@@ -29,11 +30,12 @@ import type { DialogTemplateOptions, ModalState } from "../../types";
29
30
  * @param {number} [options.showDelay] close 후 제거 지연(ms)
30
31
  * @param {number | string} [options.width] 모달 width
31
32
  * @param {StyleSpacingType} [options.padding] 모달 body padding
33
+ * @param {UseFormProps<FormContext>} [options.formContextOptions] modal 내부 Form.Provider 옵션
32
34
  * @returns {ModalState}
33
35
  * @example
34
36
  * Modal.Dialog({ stackKey: "sample", title: "제목", content: "본문" })
35
37
  */
36
- export function createDialogModal({
38
+ export function createDialogModal<FormContext extends FieldValues>({
37
39
  stackKey,
38
40
  title,
39
41
  description,
@@ -50,7 +52,8 @@ export function createDialogModal({
50
52
  showDelay,
51
53
  width,
52
54
  padding,
53
- }: DialogTemplateOptions): ModalState {
55
+ formContextOptions,
56
+ }: DialogTemplateOptions<FormContext>): ModalState<FormContext> {
54
57
  const primary = resolveDialogPrimaryButton(confirm);
55
58
 
56
59
  const headerNode =
@@ -77,6 +80,8 @@ export function createDialogModal({
77
80
  showDelay,
78
81
  width,
79
82
  padding,
83
+ // 변경 설명: 템플릿 생성 시점의 FormContext 제네릭을 modalProps로 전달한다.
84
+ formContextOptions,
80
85
  header: headerNode ?? undefined,
81
86
  body: <DialogContents content={content} className={bodyClassName} />,
82
87
  footer,
@@ -7,11 +7,13 @@ import { useAtom } from "jotai";
7
7
  import type {
8
8
  CloseFlagState,
9
9
  ModalCloseRequest,
10
+ ModalProps,
10
11
  ModalState,
11
12
  ModalStatePatch,
12
13
  UseModalReturn,
13
14
  } from "../types";
14
15
  import { modalStackAtom } from "../jotai/atoms";
16
+ import type { FieldValues } from "react-hook-form";
15
17
 
16
18
  const DEFAULT_CLOSE_DELAY = 400;
17
19
 
@@ -30,7 +32,7 @@ export function useModal(): UseModalReturn {
30
32
  const [closeFlag, setCloseFlag] = useState<CloseFlagState | null>(null);
31
33
 
32
34
  const newModal = useCallback(
33
- (newStack: ModalState) => {
35
+ <FormContext extends FieldValues>(newStack: ModalState<FormContext>) => {
34
36
  updateModalStack((stacks: ModalState[]) => {
35
37
  const exists = stacks.some(
36
38
  stack => stack.stackKey === newStack.stackKey,
@@ -45,7 +47,10 @@ export function useModal(): UseModalReturn {
45
47
  ...stacks,
46
48
  {
47
49
  ...newStack,
48
- modalProps: { ...newStack.modalProps, show: "init" },
50
+ modalProps: {
51
+ ...(newStack.modalProps as ModalProps),
52
+ show: "init",
53
+ },
49
54
  },
50
55
  ];
51
56
  });
@@ -54,14 +59,20 @@ export function useModal(): UseModalReturn {
54
59
  );
55
60
 
56
61
  const updateModal = useCallback(
57
- (nextStack: ModalStatePatch) => {
62
+ <FormContext extends FieldValues>(
63
+ nextStack: ModalStatePatch<FormContext>,
64
+ ) => {
58
65
  updateModalStack((stacks: ModalState[]) =>
59
66
  stacks.map(stack => {
60
67
  if (stack.stackKey !== nextStack.stackKey) {
61
68
  return stack;
62
69
  }
63
- const mergedProps = nextStack.modalProps
64
- ? { ...stack.modalProps, ...nextStack.modalProps }
70
+ // 변경 설명: 스택 저장 타입은 공통 ModalProps로 유지하고, 제네릭 옵션은 생성 경계에서만 보장한다.
71
+ const mergedProps: ModalProps = nextStack.modalProps
72
+ ? {
73
+ ...stack.modalProps,
74
+ ...(nextStack.modalProps as Partial<ModalProps>),
75
+ }
65
76
  : stack.modalProps;
66
77
  return {
67
78
  ...stack,
@@ -1,4 +1,5 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
+ import type { FieldValues } from "react-hook-form";
2
3
  import type {
3
4
  ModalFooterButtonAppearance,
4
5
  ModalFooterButtonDefaultOptions,
@@ -12,11 +13,13 @@ import type { ModalFooterButton } from "./footer";
12
13
  /**
13
14
  * ModalRoot 컴포넌트 props.
14
15
  * @property {string} stackKey 모달 스택 식별자
15
- * @property {ModalProps} modalProps 모달 렌더링에 필요한 상태
16
+ * @property {ModalProps<FormContext>} modalProps 모달 렌더링에 필요한 상태
16
17
  * @property {number} index Provider에서 전달한 스택 index
17
18
  * @property {string} [className] 사용자 정의 className
18
19
  */
19
- export type ModalRootProps = ModalState & {
20
+ export type ModalRootProps<
21
+ FormContext extends FieldValues = Record<string, unknown>,
22
+ > = ModalState<FormContext> & {
20
23
  /**
21
24
  * 모달 스택 식별자
22
25
  */
@@ -24,7 +27,7 @@ export type ModalRootProps = ModalState & {
24
27
  /**
25
28
  * 모달 렌더링에 필요한 상태
26
29
  */
27
- modalProps: ModalProps;
30
+ modalProps: ModalProps<FormContext>;
28
31
  /**
29
32
  * Provider에서 전달한 스택 index
30
33
  */
@@ -43,8 +46,11 @@ export type ModalRootProps = ModalState & {
43
46
  * @property {ReactNode} [footer] footer 콘텐츠
44
47
  * @property {ModalFooterButton[]} [footerButtons] footer 버튼 목록
45
48
  * @property {string} [className] 컨테이너 className
49
+ * @property {UseFormProps<FormContext>} [formContextOptions] form context 옵션
46
50
  */
47
- export type ModalContainerProps = ModalSections & {
51
+ export type ModalContainerProps<
52
+ FormContext extends FieldValues = Record<string, unknown>,
53
+ > = ModalSections<FormContext> & {
48
54
  /**
49
55
  * 모달 스택 식별자
50
56
  */
@@ -1,11 +1,12 @@
1
1
  import type { ModalCloseRequest } from "./options";
2
2
  import type { ModalState, ModalStatePatch } from "./state";
3
+ import type { FieldValues } from "react-hook-form";
3
4
 
4
5
  /**
5
6
  * close flag 상태; 모달 스택 제거 예약을 표현한다.
6
7
  * @property {string} stackKey 닫을 모달 스택 키
7
8
  * @property {number} showDelay 제거까지 대기할 지연(ms)
8
- * @property {ModalCloseRequest["callback"]} [callback] 완료 후 실행 콜백
9
+ * @property {() => void} [callback] 완료 후 실행 콜백
9
10
  */
10
11
  export type CloseFlagState = {
11
12
  /**
@@ -19,14 +20,14 @@ export type CloseFlagState = {
19
20
  /**
20
21
  * 완료 후 실행 콜백
21
22
  */
22
- callback?: ModalCloseRequest["callback"];
23
+ callback?: () => void;
23
24
  };
24
25
 
25
26
  /**
26
27
  * useModal 훅 반환값.
27
28
  * @property {ModalState[]} modalStacks 현재 모달 스택 목록
28
- * @property {(newStack: ModalState) => void} newModal 신규 모달 추가 함수
29
- * @property {(nextStack: ModalStatePatch) => void} updateModal 스택 상태 갱신 함수
29
+ * @property {<FormContext extends FieldValues>(newStack: ModalState<FormContext>) => void} newModal 신규 모달 추가 함수
30
+ * @property {<FormContext extends FieldValues>(nextStack: ModalStatePatch<FormContext>) => void} updateModal 스택 상태 갱신 함수
30
31
  * @property {(request: ModalCloseRequest) => void} closeModal close 제어 함수
31
32
  */
32
33
  export type UseModalReturn = {
@@ -37,11 +38,15 @@ export type UseModalReturn = {
37
38
  /**
38
39
  * 신규 모달 추가 함수
39
40
  */
40
- newModal: (newStack: ModalState) => void;
41
+ newModal: <FormContext extends FieldValues>(
42
+ newStack: ModalState<FormContext>,
43
+ ) => void;
41
44
  /**
42
45
  * 스택 상태 갱신 함수
43
46
  */
44
- updateModal: (nextStack: ModalStatePatch) => void;
47
+ updateModal: <FormContext extends FieldValues>(
48
+ nextStack: ModalStatePatch<FormContext>,
49
+ ) => void;
45
50
  /**
46
51
  * close 제어 함수
47
52
  */
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import type { ModalFooterButton } from "./footer";
4
4
  import type { StyleSpacingType } from "@uniai-fe/util-functions";
5
+ import type { FieldValues, UseFormProps } from "react-hook-form";
5
6
 
6
7
  /**
7
8
  * Modal 노출 상태 표현값.
@@ -16,8 +17,11 @@ export type ModalShowState = "init" | boolean;
16
17
  * @property {ReactNode} [footer] footer 콘텐츠
17
18
  * @property {number | string} [width] 커스텀 width
18
19
  * @property {StyleSpacingType} [padding] 커스텀 padding
20
+ * @property {UseFormProps<FormContext>} [formContextOptions] form context 옵션
19
21
  */
20
- export type ModalSections = {
22
+ export type ModalSections<
23
+ FormContext extends FieldValues = Record<string, unknown>,
24
+ > = {
21
25
  /**
22
26
  * 헤더 콘텐츠
23
27
  */
@@ -44,6 +48,15 @@ export type ModalSections = {
44
48
  * - [상, 우, 하, 좌]
45
49
  */
46
50
  padding?: StyleSpacingType;
51
+ /**
52
+ * form context 옵션
53
+ * - 옵션이 활성화되면, Form.Provider 사용으로 간주하여 활성화시킴
54
+ * ```
55
+ * // 다음과 같이 전달
56
+ * <Form.Provider options={formContextOptions}>
57
+ * ```
58
+ */
59
+ formContextOptions?: UseFormProps<FormContext>;
47
60
  };
48
61
 
49
62
  /**
@@ -56,8 +69,11 @@ export type ModalSections = {
56
69
  * @property {ModalShowState} show 노출 상태
57
70
  * @property {number} [showDelay] close 후 제거 지연(ms)
58
71
  * @property {ModalFooterButton[]} [footerButtons] footer 버튼 스펙
72
+ * @property {UseFormProps<FormContext>} [formContextOptions] form context 옵션
59
73
  */
60
- export type ModalProps = ModalSections & {
74
+ export type ModalProps<
75
+ FormContext extends FieldValues = Record<string, unknown>,
76
+ > = ModalSections<FormContext> & {
61
77
  /**
62
78
  * 노출 상태
63
79
  */
@@ -75,10 +91,12 @@ export type ModalProps = ModalSections & {
75
91
  /**
76
92
  * Modal 스택에 저장되는 상태.
77
93
  * @property {string} stackKey 모달 스택 키
78
- * @property {ModalProps} modalProps 컨테이너 props
94
+ * @property {ModalProps<FormContext>} modalProps 컨테이너 props
79
95
  * @property {string} [className] 추가 className
80
96
  */
81
- export type ModalState = {
97
+ export type ModalState<
98
+ FormContext extends FieldValues = Record<string, unknown>,
99
+ > = {
82
100
  /**
83
101
  * 모달 스택 키
84
102
  */
@@ -86,7 +104,7 @@ export type ModalState = {
86
104
  /**
87
105
  * 컨테이너 props
88
106
  */
89
- modalProps: ModalProps;
107
+ modalProps: ModalProps<FormContext>;
90
108
  /**
91
109
  * 추가 className
92
110
  */
@@ -96,10 +114,12 @@ export type ModalState = {
96
114
  /**
97
115
  * Modal 상태 패치 데이터.
98
116
  * @property {string} stackKey 수정할 모달 스택 키
99
- * @property {Partial<ModalProps>} [modalProps] 덮어쓸 props
117
+ * @property {Partial<ModalProps<FormContext>>} [modalProps] 덮어쓸 props
100
118
  * @property {string} [className] 추가 className
101
119
  */
102
- export type ModalStatePatch = {
120
+ export type ModalStatePatch<
121
+ FormContext extends FieldValues = Record<string, unknown>,
122
+ > = {
103
123
  /**
104
124
  * 수정할 모달 스택 키
105
125
  */
@@ -107,7 +127,7 @@ export type ModalStatePatch = {
107
127
  /**
108
128
  * 덮어쓸 props
109
129
  */
110
- modalProps?: Partial<ModalProps>;
130
+ modalProps?: Partial<ModalProps<FormContext>>;
111
131
  /**
112
132
  * 추가 className
113
133
  */
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { FieldValues, UseFormProps } from "react-hook-form";
2
3
 
3
4
  import type {
4
5
  ModalFooterButtonPosition,
@@ -56,7 +57,9 @@ export type ModalTemplateButtonSpec = {
56
57
  * @property {number | string} [width] 커스텀 width
57
58
  * @property {StyleSpacingType} [padding] 커스텀 padding
58
59
  */
59
- type ModalTemplateBase = {
60
+ type ModalTemplateBase<
61
+ FormContext extends FieldValues = Record<string, unknown>,
62
+ > = {
60
63
  /**
61
64
  * 모달 스택 키
62
65
  */
@@ -79,6 +82,11 @@ type ModalTemplateBase = {
79
82
  * - [상, 우, 하, 좌]
80
83
  */
81
84
  padding?: StyleSpacingType;
85
+ /**
86
+ * modal 내부 Form.Provider 옵션
87
+ * - 타입 안정성은 Modal.Alert/Dialog 생성 시점 제네릭으로 보장한다.
88
+ */
89
+ formContextOptions?: UseFormProps<FormContext>;
82
90
  };
83
91
 
84
92
  /**
@@ -91,8 +99,11 @@ type ModalTemplateBase = {
91
99
  * @property {ReactNode} [footer] 커스텀 footer
92
100
  * @property {number | string} [width] 커스텀 width
93
101
  * @property {StyleSpacingType} [padding] 커스텀 padding
102
+ * @property {UseFormProps<FormContext>} [formContextOptions] modal 내부 Form.Provider 옵션
94
103
  */
95
- export type AlertTemplateOptions = ModalTemplateBase & {
104
+ export type AlertTemplateOptions<
105
+ FormContext extends FieldValues = Record<string, unknown>,
106
+ > = ModalTemplateBase<FormContext> & {
96
107
  /**
97
108
  * 본문 메시지
98
109
  */
@@ -109,6 +120,10 @@ export type AlertTemplateOptions = ModalTemplateBase & {
109
120
  * 커스텀 footer
110
121
  */
111
122
  footer?: ReactNode;
123
+ /**
124
+ * modal 내부 Form.Provider 옵션
125
+ */
126
+ formContextOptions?: UseFormProps<FormContext>;
112
127
  };
113
128
 
114
129
  /**
@@ -129,8 +144,11 @@ export type AlertTemplateOptions = ModalTemplateBase & {
129
144
  * @property {ReactNode} [footer] 커스텀 footer
130
145
  * @property {number | string} [width] 커스텀 width
131
146
  * @property {StyleSpacingType} [padding] 커스텀 padding
147
+ * @property {UseFormProps<FormContext>} [formContextOptions] modal 내부 Form.Provider 옵션
132
148
  */
133
- export type DialogTemplateOptions = ModalTemplateBase & {
149
+ export type DialogTemplateOptions<
150
+ FormContext extends FieldValues = Record<string, unknown>,
151
+ > = ModalTemplateBase<FormContext> & {
134
152
  /**
135
153
  * 헤더 타이틀
136
154
  */
@@ -179,10 +197,16 @@ export type DialogTemplateOptions = ModalTemplateBase & {
179
197
  * 커스텀 footer
180
198
  */
181
199
  footer?: ReactNode;
200
+ /**
201
+ * modal 내부 Form.Provider 옵션
202
+ */
203
+ formContextOptions?: UseFormProps<FormContext>;
182
204
  };
183
205
 
184
206
  /**
185
207
  * Modal 템플릿 결과 타입.
186
208
  * @typedef {ModalState} ModalTemplateResult
187
209
  */
188
- export type ModalTemplateResult = ModalState;
210
+ export type ModalTemplateResult<
211
+ FormContext extends FieldValues = Record<string, unknown>,
212
+ > = ModalState<FormContext>;
@@ -42,6 +42,10 @@ export default function PageHeaderSettingButton({
42
42
  );
43
43
  const pathname = usePathname();
44
44
  const resolvedPath = pathname ?? "";
45
+ const closestRoute = useMemo(
46
+ () => getClosestRoute(menuItems, resolvedPath),
47
+ [menuItems, resolvedPath],
48
+ );
45
49
 
46
50
  const dropdownItems: DropdownTemplateItem[] = useMemo(
47
51
  () =>
@@ -49,19 +53,12 @@ export default function PageHeaderSettingButton({
49
53
  id: item.routeKey,
50
54
  label: item.name,
51
55
  left: item.icon,
56
+ // 변경: Dropdown.Template 선택 계약은 items[].selected를 source로 사용한다.
57
+ selected: String(closestRoute?.routeKey) === String(item.routeKey),
52
58
  })),
53
- [menuItems],
59
+ [closestRoute?.routeKey, menuItems],
54
60
  );
55
61
 
56
- /**
57
- * 현재 경로와 가장 근접한 설정 경로를 계산한다.
58
- * - 메뉴 수가 많아져도 useMemo로 계산을 한 번만 수행한다.
59
- */
60
- const selectedIds = useMemo(() => {
61
- const closestRoute = getClosestRoute(menuItems, resolvedPath);
62
- return closestRoute ? [String(closestRoute.routeKey)] : [];
63
- }, [menuItems, resolvedPath]);
64
-
65
62
  /**
66
63
  * dropdown 항목을 선택했을 때 routeKey에 대응하는 onSelect를 실행한다.
67
64
  * @param {DropdownTemplateChangePayload} payload 선택 결과 payload
@@ -98,7 +95,6 @@ export default function PageHeaderSettingButton({
98
95
  items={dropdownItems}
99
96
  width={170}
100
97
  size="small"
101
- selectedIds={selectedIds}
102
98
  // Dropdown.Template 신규 계약(onChange payload)을 우선 사용한다.
103
99
  onChange={handleSelect}
104
100
  containerProps={{