@uniai-fe/uds-primitives 0.12.4 → 0.12.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.
Files changed (55) hide show
  1. package/README.md +17 -0
  2. package/dist/styles.css +2 -2
  3. package/package.json +17 -17
  4. package/src/components/form/markup/Provider.tsx +7 -3
  5. package/src/components/form/markup/form-field/Body.tsx +1 -0
  6. package/src/components/form/markup/form-field/Container.tsx +3 -1
  7. package/src/components/form/markup/form-field/Footer.tsx +1 -0
  8. package/src/components/form/markup/form-field/Header.tsx +2 -0
  9. package/src/components/form/markup/form-field/Template.tsx +3 -2
  10. package/src/components/form/markup/form-field/index.tsx +1 -1
  11. package/src/components/form/markup/index.tsx +1 -0
  12. package/src/components/form/styles/form-field/variables.scss +2 -2
  13. package/src/components/form/types/props.ts +21 -19
  14. package/src/components/form/utils/form-field.ts +6 -4
  15. package/src/components/input/markup/date/Template.tsx +83 -33
  16. package/src/components/input/markup/date/Trigger.tsx +28 -12
  17. package/src/components/input/markup/date/range/Template.tsx +94 -35
  18. package/src/components/input/markup/time/Template.tsx +118 -5
  19. package/src/components/input/markup/time/Trigger.tsx +35 -2
  20. package/src/components/input/types/date.ts +176 -33
  21. package/src/components/input/types/time.ts +10 -4
  22. package/src/components/slot/index.tsx +3 -6
  23. package/src/components/slot/markup/Base.tsx +146 -9
  24. package/src/components/slot/markup/index.tsx +2 -4
  25. package/src/components/slot/types/props.ts +119 -23
  26. package/src/components/table/types/foundation.ts +9 -7
  27. package/src/components/time-picker/markup/Footer.tsx +5 -5
  28. package/src/components/time-picker/markup/HourSection.tsx +7 -7
  29. package/src/components/time-picker/markup/MinuteSection.tsx +5 -5
  30. package/src/components/time-picker/markup/Summary.tsx +2 -2
  31. package/src/components/time-picker/markup/Template.tsx +11 -11
  32. package/src/components/time-picker/types/time-picker.ts +6 -0
  33. package/src/components/time-picker/utils/time.ts +21 -9
  34. package/src/index.scss +0 -2
  35. package/src/index.tsx +8 -2
  36. package/src/types/index.ts +1 -4
  37. package/src/utils/index.ts +8 -4
  38. package/src/utils/selected-values.ts +20 -14
  39. package/src/components/scrollbar/hooks/index.ts +0 -4
  40. package/src/components/scrollbar/img/.gitkeep +0 -0
  41. package/src/components/scrollbar/index.scss +0 -1
  42. package/src/components/scrollbar/index.tsx +0 -4
  43. package/src/components/scrollbar/markup/index.tsx +0 -4
  44. package/src/components/scrollbar/styles/index.scss +0 -0
  45. package/src/components/scrollbar/types/index.ts +0 -4
  46. package/src/components/scrollbar/utils/index.ts +0 -4
  47. package/src/components/spinner/hooks/index.ts +0 -4
  48. package/src/components/spinner/img/.gitkeep +0 -0
  49. package/src/components/spinner/index.scss +0 -1
  50. package/src/components/spinner/index.tsx +0 -4
  51. package/src/components/spinner/markup/index.tsx +0 -4
  52. package/src/components/spinner/styles/index.scss +0 -0
  53. package/src/components/spinner/types/index.ts +0 -4
  54. package/src/components/spinner/utils/index.ts +0 -4
  55. package/src/hooks/index.ts +0 -4
package/README.md CHANGED
@@ -46,6 +46,23 @@ Button은 기존 priority 범위를 유지한다.
46
46
  `Input.Time.Template`과 standalone `TimePicker.Template`, `TimePickerUtils`도 root public
47
47
  entry에서 제공한다.
48
48
 
49
+ Select와 Dropdown이 함께 사용하는 `SelectedValue`, `toSelectedValueKey`,
50
+ `isSameSelectedValue`, `isSameSelectedValueList`, `normalizeSingleSelectedValue`,
51
+ `normalizeSelectedValuesByMode`도 root public entry에서 제공한다.
52
+
53
+ `Slot.Base`는 `as`로 렌더링 요소를 교체하거나, `asChild`로 단일 React element에
54
+ className, style, event handler와 ref를 병합해 wrapper 없이 렌더링한다.
55
+
56
+ ```tsx
57
+ import { Slot } from "@uniai-fe/uds-primitives";
58
+
59
+ <Slot.Base asChild className="card-link">
60
+ <a href="/docs">문서 보기</a>
61
+ </Slot.Base>;
62
+ ```
63
+
64
+ `asChild`에서 anchor/button 같은 element 전용 prop은 child에 둔다.
65
+
49
66
  ## Styles
50
67
 
51
68
  CSS consumer는 Foundation 다음에 Primitives를 로드한다.
package/dist/styles.css CHANGED
@@ -442,8 +442,8 @@
442
442
  --dropdown-text-weight-selected-xsmall: var(--dropdown-text-weight);
443
443
  --form-field-width: auto;
444
444
  --form-field-flex: 0 0 auto;
445
- --form-field-gap-x: var(--spacing-gap-5, 12px);
446
- --form-field-gap-y: var(--spacing-gap-3, 6px);
445
+ --form-field-gap-x: var(--spacing-gap-5);
446
+ --form-field-gap-y: var(--spacing-gap-3);
447
447
  --form-field-label-font-size: var(--font-label-small-size);
448
448
  --form-field-label-font-weight: 400;
449
449
  --form-field-label-line-height: var(--font-label-small-line-height);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -46,7 +46,7 @@
46
46
  "@mantine/core": ">=8 <9",
47
47
  "@mantine/dates": ">=8 <9",
48
48
  "@mantine/hooks": ">=8 <9",
49
- "@uniai-fe/uds-foundation": "^0.4.8 || ^0.5.0",
49
+ "@uniai-fe/uds-foundation": "^0.4.8 || ^0.5.0 || ^0.6.0",
50
50
  "@uniai-fe/util-functions": "^0.3.0 || ^0.4.0",
51
51
  "react": ">=19 <20",
52
52
  "react-daum-postcode": "^4.0.0",
@@ -63,13 +63,13 @@
63
63
  "dayjs": "^1.11.20"
64
64
  },
65
65
  "dependencies": {
66
- "@radix-ui/react-checkbox": "^1.3.8",
67
- "@radix-ui/react-dropdown-menu": "^2.1.21",
68
- "@radix-ui/react-popover": "^1.1.20",
69
- "@radix-ui/react-radio-group": "^1.4.4",
70
- "@radix-ui/react-tabs": "^1.1.18",
71
- "@radix-ui/react-tooltip": "^1.2.13",
72
- "@radix-ui/react-visually-hidden": "^1.2.8",
66
+ "@radix-ui/react-checkbox": "^1.3.11",
67
+ "@radix-ui/react-dropdown-menu": "^2.1.24",
68
+ "@radix-ui/react-popover": "^1.1.23",
69
+ "@radix-ui/react-radio-group": "^1.4.7",
70
+ "@radix-ui/react-tabs": "^1.1.21",
71
+ "@radix-ui/react-tooltip": "^1.2.16",
72
+ "@radix-ui/react-visually-hidden": "^1.2.11",
73
73
  "clsx": "^2.1.1",
74
74
  "dayjs": "^1.11.21",
75
75
  "react-daum-postcode": "^4.0.0"
@@ -80,18 +80,18 @@
80
80
  "@mantine/hooks": "^8.3.18",
81
81
  "@svgr/webpack": "^8.1.0",
82
82
  "@types/node": "^24.13.3",
83
- "@types/react": "^19.2.17",
84
- "@types/react-dom": "^19.2.3",
83
+ "@types/react": "^19.2.18",
84
+ "@types/react-dom": "^19.2.4",
85
85
  "eslint": "^9.39.5",
86
86
  "prettier": "^3.9.6",
87
- "react-hook-form": "^7.82.0",
88
- "sass": "^1.101.3",
87
+ "react-hook-form": "^7.84.0",
88
+ "sass": "^1.101.7",
89
89
  "typescript": "6.0.3",
90
- "@uniai-fe/eslint-config": "0.4.2",
91
- "@uniai-fe/uds-foundation": "0.5.0",
92
90
  "@uniai-fe/next-devkit": "0.4.0",
93
- "@uniai-fe/tsconfig": "0.2.0",
94
- "@uniai-fe/util-functions": "0.4.3"
91
+ "@uniai-fe/eslint-config": "0.4.2",
92
+ "@uniai-fe/uds-foundation": "0.6.0",
93
+ "@uniai-fe/util-functions": "0.4.3",
94
+ "@uniai-fe/tsconfig": "0.2.0"
95
95
  },
96
96
  "scripts": {
97
97
  "check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect } from "react";
4
- // import { useEffect } from "react";
5
4
  import {
6
5
  FormProvider as ReactHookFormProvider,
7
6
  useForm,
@@ -11,7 +10,13 @@ import {
11
10
  } from "react-hook-form";
12
11
 
13
12
  /**
14
- * react-hook-form; context provider
13
+ * Form Component; react-hook-form context provider
14
+ * @component
15
+ * @param {object} props Form provider props
16
+ * @property {React.ReactNode} props.children provider 하위 콘텐츠
17
+ * @property {UseFormProps<FormDataType>} [props.options] useForm 초기 옵션
18
+ * @property {(params: UseFormReturn<FormDataType>) => void | undefined} [props.callback] 생성된 form methods 전달 callback
19
+ * @desc useForm methods를 생성하고 React Hook Form context로 제공하며 callback이 있으면 effect에서 methods를 전달한다.
15
20
  */
16
21
  export default function FormProvider<FormDataType extends FieldValues>({
17
22
  children,
@@ -27,7 +32,6 @@ export default function FormProvider<FormDataType extends FieldValues>({
27
32
 
28
33
  useEffect(() => {
29
34
  if (typeof callback === "undefined") return;
30
- // if (typeof callback !== "undefined")
31
35
  callback(formMethods);
32
36
  }, [callback, formMethods]);
33
37
 
@@ -6,6 +6,7 @@ import clsx from "clsx";
6
6
  * @param {object} props
7
7
  * @property {string} [className]
8
8
  * @property {React.ReactNode} [children]
9
+ * @desc field control과 주요 콘텐츠를 body 영역에 렌더링한다.
9
10
  */
10
11
  export default function FormFieldBody({
11
12
  className,
@@ -13,10 +13,12 @@ import {
13
13
  * @component
14
14
  * @param {FormFieldContainerProps} props
15
15
  * @property {React.ElementType} [as] container 태그 지정 (default: <section />)
16
- * @property {FormFieldWidth} [width] form field 너비 옵션
16
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [width] form field 너비 옵션
17
+ * @property {"default" | "error"} [state] visual state
17
18
  * @property {string} [className]
18
19
  * @property {React.ReactNode} [children]
19
20
  * @property {React.HTMLAttributes<HTMLElement>} [containerProps] container 속성
21
+ * @desc width를 data attribute와 CSS variable로 변환하고 visual state를 노출하는 field wrapper다.
20
22
  */
21
23
  const FormFieldContainer = forwardRef<HTMLElement, FormFieldContainerProps>(
22
24
  (
@@ -7,6 +7,7 @@ import type { FormFieldFooterProps } from "../../types";
7
7
  * @param {FormFieldFooterProps} props
8
8
  * @property {string} [className]
9
9
  * @property {React.ReactNode} [children]
10
+ * @desc helper 또는 action 콘텐츠와 footer DOM 속성을 렌더링한다.
10
11
  */
11
12
  export default function FormFieldFooter({
12
13
  className,
@@ -10,11 +10,13 @@ import { Slot } from "../../../slot";
10
10
  * @param {FormFieldHeaderProps} props
11
11
  * @property {string} [className]
12
12
  * @property {string} [label] 필드명
13
+ * @property {React.ReactNode} [labelJsx] label보다 우선하는 커스텀 label JSX
13
14
  * @property {React.ElementType} [labelAs] 필드 태그 지정 (default: <h5 />)
14
15
  * @property {string} [labelId] 필드 아이디 지정
15
16
  * @property {boolean} [required] 필수 여부
16
17
  * @property {React.ReactNode} [children] 추가 헤더 내용
17
18
  * @property {React.HTMLAttributes<HTMLElement>} [labelProps] 추가 라벨 속성
19
+ * @desc labelJsx를 우선 렌더링하고 문자열 label은 Slot.Text로 감싸 필수 표시와 함께 구성한다.
18
20
  */
19
21
  export default function FormFieldHeader({
20
22
  className,
@@ -17,10 +17,11 @@ import FormFieldContainer from "./Container";
17
17
  * @property {React.ReactNode} [footer] 푸터 내용
18
18
  * @property {FormFieldHeaderProps} [headerProps] 헤더 속성
19
19
  * @property {FormFieldFooterProps} [footerProps] 푸터 속성
20
- * @property {FormFieldWidth} [width] form field 너비 옵션
21
- * @property {FormFieldState} [state] visual state
20
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [width] form field 너비 옵션
21
+ * @property {"default" | "error"} [state] visual state
22
22
  * @property {React.HTMLAttributes<HTMLElement>} [containerProps] container 속성
23
23
  * @property {React.ReactNode} children
24
+ * @desc Header와 Body를 렌더링하고 footer가 있으면 Footer를 추가해 field layout을 구성한다.
24
25
  */
25
26
  const FormFieldTemplate = forwardRef<HTMLElement, FormFieldTemplateProps>(
26
27
  (
@@ -6,7 +6,7 @@ import FormFieldTemplate from "./Template";
6
6
 
7
7
  /**
8
8
  * Form; form field 컴포넌트
9
- * @component
9
+ * @namespace Form.Field
10
10
  * @desc
11
11
  * - FormField.Container: Form 필드의 컨테이너 컴포넌트
12
12
  * - FormField.Header: Form 필드의 헤더 컴포넌트
@@ -3,6 +3,7 @@ import { FormField } from "./form-field";
3
3
 
4
4
  /**
5
5
  * Form; provider + field layout namespace
6
+ * @namespace Form
6
7
  * @desc
7
8
  * - `Form.Provider`: react-hook-form provider 래퍼다.
8
9
  * - `Form.Field.Container`: field 루트 wrapper다.
@@ -2,8 +2,8 @@
2
2
  --form-field-width: auto;
3
3
  --form-field-flex: 0 0 auto;
4
4
 
5
- --form-field-gap-x: var(--spacing-gap-5, 12px);
6
- --form-field-gap-y: var(--spacing-gap-3, 6px);
5
+ --form-field-gap-x: var(--spacing-gap-5);
6
+ --form-field-gap-y: var(--spacing-gap-3);
7
7
 
8
8
  --form-field-label-font-size: var(--font-label-small-size);
9
9
  --form-field-label-font-weight: 400;
@@ -1,18 +1,14 @@
1
1
  import type { ElementType } from "react";
2
2
 
3
3
  /**
4
- * Form; field container width option
5
- * - full: 상위 영역을 100%로 채움
6
- * - fit: 콘텐츠 너비에 맞춤
7
- * - fill: flex-grow 1 분배
8
- * - auto: 기본 width(auto)
9
- * - number: px 단위 입력, rem으로 환산
10
- * - string: CSS width 문자열
4
+ * Form Types; field container width option
5
+ * @desc `"full" | "fit" | "fill" | "auto" | number | string`을 허용한다. number는 px 기준 rem으로 환산하고 preset이 아닌 string은 CSS width로 사용한다.
11
6
  */
12
7
  export type FormFieldWidth = "full" | "fit" | "fill" | "auto" | number | string;
13
8
 
14
9
  /**
15
- * Form; field state option
10
+ * Form Types; field state option
11
+ * @desc `"default" | "error"`를 허용하며 field helper의 visual state를 지정한다.
16
12
  */
17
13
  export type FormFieldState = "default" | "error";
18
14
 
@@ -26,6 +22,7 @@ export type FormFieldState = "default" | "error";
26
22
  * @property {React.HTMLAttributes<HTMLElement>} [labelProps] label 태그 속성
27
23
  * @property {boolean} [required] 필수 표시
28
24
  * @property {React.ReactNode} [children] description 등 추가 요소
25
+ * @desc field label, 필수 표시와 header 추가 콘텐츠를 구성한다.
29
26
  */
30
27
  export interface FormFieldHeaderProps extends React.HTMLAttributes<HTMLElement> {
31
28
  /**
@@ -33,7 +30,7 @@ export interface FormFieldHeaderProps extends React.HTMLAttributes<HTMLElement>
33
30
  */
34
31
  className?: string;
35
32
  /**
36
- * label heading 태그
33
+ * label heading 태그를 지정한다.
37
34
  */
38
35
  labelAs?: ElementType;
39
36
  /**
@@ -41,11 +38,11 @@ export interface FormFieldHeaderProps extends React.HTMLAttributes<HTMLElement>
41
38
  */
42
39
  labelId?: string;
43
40
  /**
44
- * 기본 label 텍스트
41
+ * 기본 label 텍스트를 지정한다.
45
42
  */
46
43
  label?: string;
47
44
  /**
48
- * 커스텀 label JSX
45
+ * label보다 우선하는 커스텀 label JSX를 지정한다.
49
46
  */
50
47
  labelJsx?: React.ReactNode;
51
48
  /**
@@ -66,6 +63,7 @@ export interface FormFieldHeaderProps extends React.HTMLAttributes<HTMLElement>
66
63
  * Form.Field Footer props; `Footer.tsx`에서 helper/action 영역을 담당한다.
67
64
  * @property {string} [className] footer className
68
65
  * @property {React.ReactNode} [children] footer 내용
66
+ * @desc field helper 또는 action을 footer 영역에 렌더링한다.
69
67
  */
70
68
  export interface FormFieldFooterProps extends React.HTMLAttributes<HTMLElement> {
71
69
  /**
@@ -81,10 +79,12 @@ export interface FormFieldFooterProps extends React.HTMLAttributes<HTMLElement>
81
79
  /**
82
80
  * Form.Field Container props; `Container.tsx`에서 wrapper와 width 옵션을 제어한다.
83
81
  * @property {string} [className] container className
84
- * @property {React.ElementType} [as] container 태그
85
- * @property {FormFieldWidth} [width] width 옵션(full/fit/fill/auto/custom)
86
- * @property {FormFieldState} [state] visual state
82
+ * @property {ElementType} [as] container 태그
83
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [width] width preset 또는 custom width
84
+ * @property {"default" | "error"} [state] visual state
87
85
  * @property {React.ReactNode} children Body에 전달될 콘텐츠
86
+ * @see React.HTMLAttributes<HTMLElement>
87
+ * @desc semantic wrapper, width와 helper visual state를 제어한다.
88
88
  */
89
89
  export interface FormFieldContainerProps extends React.HTMLAttributes<HTMLElement> {
90
90
  /**
@@ -96,11 +96,11 @@ export interface FormFieldContainerProps extends React.HTMLAttributes<HTMLElemen
96
96
  */
97
97
  as?: ElementType;
98
98
  /**
99
- * width 옵션
99
+ * `"full" | "fit" | "fill" | "auto" | number | string` width 옵션을 지정한다.
100
100
  */
101
101
  width?: FormFieldWidth;
102
102
  /**
103
- * visual state
103
+ * `"default" | "error"` visual state를 지정한다.
104
104
  */
105
105
  state?: FormFieldState;
106
106
  /**
@@ -115,10 +115,12 @@ export interface FormFieldContainerProps extends React.HTMLAttributes<HTMLElemen
115
115
  * @property {FormFieldHeaderProps} [headerProps] Header props
116
116
  * @property {FormFieldFooterProps} [footerProps] Footer props
117
117
  * @property {string} [className] container className
118
- * @property {React.ElementType} [as] container 태그
119
- * @property {FormFieldWidth} [width] width 옵션(full/fit/fill/auto/custom)
120
- * @property {FormFieldState} [state] visual state
118
+ * @property {ElementType} [as] container 태그
119
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [width] width preset 또는 custom width
120
+ * @property {"default" | "error"} [state] visual state
121
121
  * @property {React.ReactNode} children Body에 전달될 콘텐츠
122
+ * @see FormFieldContainerProps
123
+ * @desc Container props를 상속하고 Header, Body와 선택적 Footer를 조합한다.
122
124
  */
123
125
  export interface FormFieldTemplateProps extends FormFieldContainerProps {
124
126
  /**
@@ -6,8 +6,9 @@ const WIDTH_PRESETS = new Set<FormFieldWidth>(["full", "fit", "fill", "auto"]);
6
6
  /**
7
7
  * Form.Field width attr helper; preset이면 그대로 반환하고 custom이면 "custom"/기본값은 "auto".
8
8
  * @function
9
- * @param {FormFieldWidth} [width]
10
- * @returns {string}
9
+ * @param {"full" | "fit" | "fill" | "auto" | number | string} [width] width preset 또는 custom width
10
+ * @desc Form.Field width를 data-width attribute 값으로 변환한다.
11
+ * @return {string} preset, "custom" 또는 "auto"
11
12
  */
12
13
  export const getFormFieldWidthAttr = (width?: FormFieldWidth): string => {
13
14
  if (typeof width === "string" && WIDTH_PRESETS.has(width)) {
@@ -24,8 +25,9 @@ export const getFormFieldWidthAttr = (width?: FormFieldWidth): string => {
24
25
  /**
25
26
  * Form.Field width style helper; number/string custom 값을 CSS width 문자열로 환산한다.
26
27
  * @function
27
- * @param {FormFieldWidth} [width]
28
- * @returns {string | undefined}
28
+ * @param {"full" | "fit" | "fill" | "auto" | number | string} [width] width preset 또는 custom width
29
+ * @desc 유한한 number는 rem으로 환산하고 preset이 아닌 string 그대로 반환한다.
30
+ * @return {string | undefined} custom CSS width 또는 preset/미지정일 때 undefined
29
31
  */
30
32
  export const getFormFieldWidthValue = (
31
33
  width?: FormFieldWidth,
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
 
3
- import type { ChangeEvent, MouseEvent as ReactMouseEvent } from "react";
3
+ import type {
4
+ ChangeEvent,
5
+ FocusEvent as ReactFocusEvent,
6
+ MouseEvent as ReactMouseEvent,
7
+ } from "react";
4
8
  import { forwardRef, useCallback, useMemo, useState } from "react";
5
9
  import { useUncontrolled } from "@mantine/hooks";
6
10
  import { Calendar } from "../../../calendar";
@@ -26,30 +30,39 @@ const INPUT_DATE_TABLE_FORMAT = "YY-MM-DD";
26
30
  * placeholder는 format과 별도 props로 처리되며, format 변경으로 placeholder가 자동 치환되지는 않는다.
27
31
  * @component
28
32
  * @param {InputCalendarProps} props
29
- * @param {CalendarMode} [props.mode="date"] 날짜/시간 모드
30
- * @param {CalendarColumns} [props.columns=1] 동시 노출 달력 수
31
- * @param {CalendarValue} [props.value] 제어형 값
32
- * @param {CalendarValue} [props.defaultValue] 비제어 초기값
33
- * @param {CalendarOnChange} [props.onChange] 값 변경 이벤트
34
- * @param {CalendarOnChange} [props.onValueChange] onChange alias
35
- * @param {boolean} [props.readOnly] 읽기 전용 여부
36
- * @param {boolean} [props.disabled] 비활성화 여부
37
- * @param {CalendarDatePickerProps} [props.datePickerProps] Mantine DatePicker 옵션
38
- * @param {string} [props.name] form name/RHF name
39
- * @param {UseFormRegisterReturn} [props.register] RHF register
40
- * @param {string} [props.placeholder="YYYY-MM-DD"] placeholder
41
- * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] trigger input priority
42
- * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] trigger input state
43
- * @param {ReactNode} [props.header] 패널 header 콘텐츠
44
- * @param {ReactNode} [props.footer] 패널 footer 콘텐츠
45
- * @param {InputCalendarTexts} [props.texts] 기본 Date 문구
46
- * @param {unknown} [props.timePicker] TimePicker 확장용 예약 슬롯(현재 미구현)
47
- * @param {boolean} [props.calendarOpened] calendar 열림 제어 상태
48
- * @param {(open: boolean) => void} [props.onCalendarOpen] calendar 열림 변경 이벤트
49
- * @param {(event: MouseEvent<Element>) => void} [props.onClick] trigger 클릭 핸들러
50
- * @param {string} [props.id] trigger id
51
- * @param {ReactNode} [props.trigger] 커스텀 trigger 슬롯
52
- * @param {(props: InputCalendarTriggerRenderProps) => ReactNode} [props.renderTrigger] 커스텀 trigger 렌더 함수
33
+ * @property {"date" | "date-time" | "time"} [props.mode="date"] Calendar layout mode. 선택 body는 date로 유지된다.
34
+ * @property {1 | 2} [props.columns=1] 동시 노출 달력 수
35
+ * @property {string | null} [props.value] 제어형 날짜
36
+ * @property {string | null} [props.defaultValue] 비제어 초기 날짜 값
37
+ * @property {(value: string | null) => void} [props.onChange] 값 변경 이벤트
38
+ * @property {(value: string | null) => void} [props.onValueChange] onChange alias
39
+ * @property {boolean} [props.readOnly] 읽기 전용 여부
40
+ * @property {boolean} [props.disabled] 비활성화 여부
41
+ * @property {CalendarDatePickerProps} [props.datePickerProps] Mantine DatePicker 옵션
42
+ * @property {string} [props.name] hidden input form name
43
+ * @property {string} [props.form] hidden input form id
44
+ * @property {UseFormRegisterReturn} [props.register] hidden input RHF register
45
+ * @property {boolean} [props.required] hidden input required 여부
46
+ * @property {InputCalendarHiddenInputProps} [props.hiddenInputProps] Template 고정값을 제외한 hidden input native props
47
+ * @property {string} [props.placeholder="YYYY-MM-DD"] placeholder
48
+ * @property {"primary" | "secondary" | "tertiary" | "table"} [props.priority="primary"] trigger input priority
49
+ * @property {"small" | "medium" | "large"} [props.size="medium"] trigger input size
50
+ * @property {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state="default"] trigger input state
51
+ * @property {boolean} [props.block=false] trigger width 100% 여부
52
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [props.width] trigger width preset 또는 custom width
53
+ * @property {(event: FocusEvent<HTMLInputElement>) => void} [props.onFocus] trigger focus 핸들러
54
+ * @property {(event: FocusEvent<HTMLInputElement>) => void} [props.onBlur] trigger blur 핸들러
55
+ * @property {ReactNode} [props.header] 패널 header 콘텐츠
56
+ * @property {ReactNode} [props.footer] 패널 footer 콘텐츠
57
+ * @property {InputCalendarTexts} [props.texts] 기본 Date 문구
58
+ * @property {unknown} [props.timePicker] 미구현 compatibility prop
59
+ * @property {boolean} [props.calendarOpened] calendar 열림 제어 상태
60
+ * @property {(open: boolean) => void} [props.onCalendarOpen] calendar 열림 변경 이벤트
61
+ * @property {(event: MouseEvent<Element>) => void} [props.onClick] trigger 클릭 핸들러
62
+ * @property {string} [props.id] trigger id
63
+ * @property {ReactNode} [props.trigger] 커스텀 trigger 슬롯
64
+ * @property {(props: InputCalendarTriggerRenderProps) => ReactNode} [props.renderTrigger] 커스텀 trigger 렌더 함수
65
+ * @desc 날짜 표시 trigger, Calendar panel과 YYYY-MM-DD hidden form storage를 조합한다.
53
66
  */
54
67
  const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
55
68
  (
@@ -64,10 +77,18 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
64
77
  disabled,
65
78
  datePickerProps,
66
79
  name,
80
+ form,
67
81
  register,
82
+ required,
83
+ hiddenInputProps,
68
84
  placeholder = "YYYY-MM-DD",
69
85
  priority = "primary",
86
+ size = "medium",
70
87
  state = "default",
88
+ block = false,
89
+ width,
90
+ onFocus,
91
+ onBlur,
71
92
  header,
72
93
  footer,
73
94
  texts,
@@ -88,7 +109,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
88
109
  * Input Date Template; Calendar open 상태 변경 전달 함수.
89
110
  * 비제어 사용을 위해 내부 상태를 갱신하고, 제어형 사용을 위해 외부 owner에게 next open 값을 전달한다.
90
111
  * @param {boolean} nextOpen 다음 calendar open 상태
91
- * @returns {void}
112
+ * @return {void}
92
113
  */
93
114
  const handleCalendarOpenChange = useCallback(
94
115
  (nextOpen: boolean) => {
@@ -113,7 +134,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
113
134
  * Input Date Template; RHF hidden input 변경 이벤트 동기화 함수.
114
135
  * trigger 표시값과 form 저장값을 분리하기 위해 CalendarValue를 직렬화한 synthetic change를 register에 전달한다.
115
136
  * @param {CalendarValue} nextValue 다음 calendar 선택 값
116
- * @returns {void}
137
+ * @return {void}
117
138
  */
118
139
  const emitRegisterChange = useCallback(
119
140
  (nextValue: CalendarValue) => {
@@ -140,7 +161,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
140
161
  * Input Date Template; calendar value 갱신 함수.
141
162
  * 내부/제어형 value pipeline을 갱신한 뒤 RHF register 저장값을 같은 CalendarValue 기준으로 동기화한다.
142
163
  * @param {CalendarValue} nextValue 다음 calendar 선택 값
143
- * @returns {void}
164
+ * @return {void}
144
165
  */
145
166
  const updateValue = useCallback(
146
167
  (nextValue: CalendarValue) => {
@@ -164,17 +185,33 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
164
185
  * Input Date Template; trigger click 위임 함수.
165
186
  * Calendar Root의 PopOver toggle은 asChild trigger props가 담당하고, 이 함수는 소비자가 넘긴 trigger onClick만 보존한다.
166
187
  * @param {ReactMouseEvent<Element>} event trigger click event
167
- * @returns {void}
188
+ * @return {void}
168
189
  */
169
190
  const handleTriggerClick = (event: ReactMouseEvent<Element>) => {
170
191
  triggerOnClick?.(event);
171
192
  };
172
193
 
194
+ /**
195
+ * Input Date Template; trigger blur와 RHF touched 상태 동기화 함수.
196
+ * 표시 trigger는 form name을 소유하지 않으므로 직렬화된 hidden input 값을 synthetic blur event로 전달한다.
197
+ * @utility
198
+ * @param {ReactFocusEvent<HTMLInputElement>} event trigger blur event
199
+ * @desc 소비자 blur callback과 hidden input register blur를 같은 날짜 값으로 동기화한다.
200
+ * @return {void}
201
+ */
202
+ const handleTriggerBlur = (event: ReactFocusEvent<HTMLInputElement>) => {
203
+ register?.onBlur({
204
+ target: { name: register.name, value: serializedCalendarValue },
205
+ type: "blur",
206
+ });
207
+ onBlur?.(event);
208
+ };
209
+
173
210
  /**
174
211
  * Input Date Template; Calendar Core change adapter 함수.
175
212
  * Calendar.Root의 onChange 값을 Template value/RHF 동기화 pipeline으로 연결한다.
176
213
  * @param {CalendarValue} nextValue 다음 calendar 선택 값
177
- * @returns {void}
214
+ * @return {void}
178
215
  */
179
216
  const handleCalendarChange = (nextValue: CalendarValue) => {
180
217
  updateValue(nextValue);
@@ -201,7 +238,12 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
201
238
  readOnly,
202
239
  onClick: handleTriggerClick,
203
240
  priority,
241
+ size,
204
242
  state,
243
+ block,
244
+ width,
245
+ onFocus,
246
+ onBlur: handleTriggerBlur,
205
247
  };
206
248
 
207
249
  const triggerNode = trigger ?? renderTrigger?.(triggerRenderProps) ?? (
@@ -214,6 +256,11 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
214
256
  readOnly={readOnly}
215
257
  onClick={handleTriggerClick}
216
258
  priority={priority}
259
+ size={size}
260
+ block={block}
261
+ width={width}
262
+ onFocus={onFocus}
263
+ onBlur={handleTriggerBlur}
217
264
  />
218
265
  );
219
266
 
@@ -228,7 +275,7 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
228
275
  readOnly={readOnly}
229
276
  value={calendarValue}
230
277
  onChange={handleCalendarChange}
231
- // 변경: Mantine DatePicker는 valueFormat prop을 지원하지 않으므로 옵션을 그대로 전달한다.
278
+ // Mantine DatePicker는 valueFormat prop을 지원하지 않으므로 옵션을 그대로 전달한다.
232
279
  datePickerProps={datePickerProps}
233
280
  header={header}
234
281
  footer={footerContent}
@@ -239,11 +286,14 @@ const InputDateTemplate = forwardRef<HTMLDivElement, InputCalendarProps>(
239
286
  </Calendar.Root>
240
287
  {(register?.name || name) && (
241
288
  <input
242
- // 변경: trigger 표시값과 저장값을 분리하기 위해 form 참여는 hidden input만 담당한다.
289
+ // trigger 표시값과 form 저장값을 분리해 hidden input만 form에 참여한다.
290
+ {...hiddenInputProps}
291
+ {...register}
243
292
  type="hidden"
244
293
  value={serializedCalendarValue}
245
294
  name={register?.name ?? name}
246
- {...register}
295
+ form={form ?? hiddenInputProps?.form}
296
+ required={required ?? hiddenInputProps?.required}
247
297
  />
248
298
  )}
249
299
  </>
@@ -19,17 +19,23 @@ import type { InputCalendarTriggerViewProps } from "../../types";
19
19
  * 그 외 priority에서는 기존처럼 right 슬롯을 유지한다.
20
20
  * @component
21
21
  * @param {InputCalendarTriggerViewProps} props
22
- * @param {string} [props.className] trigger className
23
- * @param {string} [props.placeholder] placeholder
24
- * @param {string} [props.displayValue] 표시 문자열
25
- * @param {(event: MouseEvent<Element>) => void} [props.onClick] 클릭 핸들러
26
- * @param {boolean} [props.disabled] disabled 여부
27
- * @param {boolean} [props.readOnly] readOnly 여부
28
- * @param {"primary" | "secondary" | "tertiary" | "table"} [props.priority] trigger input priority
29
- * @param {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state] trigger input state
30
- * @param {string} [props.id] input id
31
- * @param {string} [props.name] form name
32
- * @param {UseFormRegisterReturn} [props.register] RHF register
22
+ * @property {string} [props.className] trigger className
23
+ * @property {string} [props.placeholder] placeholder
24
+ * @property {string} [props.displayValue] 표시 문자열
25
+ * @property {(event: MouseEvent<Element>) => void} [props.onClick] 클릭 핸들러
26
+ * @property {boolean} [props.disabled] disabled 여부
27
+ * @property {boolean} [props.readOnly] readOnly 여부
28
+ * @property {"primary" | "secondary" | "tertiary" | "table"} [props.priority] trigger input priority
29
+ * @property {"small" | "medium" | "large"} [props.size] trigger input size
30
+ * @property {"default" | "active" | "focused" | "success" | "error" | "disabled" | "loading"} [props.state] trigger input state
31
+ * @property {boolean} [props.block] trigger width 100% 여부
32
+ * @property {"full" | "fit" | "fill" | "auto" | number | string} [props.width] trigger width preset 또는 custom width
33
+ * @property {(event: FocusEvent<HTMLInputElement>) => void} [props.onFocus] trigger focus 핸들러
34
+ * @property {(event: FocusEvent<HTMLInputElement>) => void} [props.onBlur] trigger blur 핸들러
35
+ * @property {string} [props.id] input id
36
+ * @property {string} [props.name] form name
37
+ * @property {UseFormRegisterReturn} [props.register] RHF register
38
+ * @desc Date 값을 직접 편집하지 않고 Calendar 선택값을 표시하는 기본 trigger다.
33
39
  */
34
40
  const InputDateTrigger = forwardRef<
35
41
  HTMLInputElement,
@@ -44,7 +50,12 @@ const InputDateTrigger = forwardRef<
44
50
  disabled,
45
51
  readOnly,
46
52
  priority,
53
+ size,
47
54
  state,
55
+ block,
56
+ width,
57
+ onFocus,
58
+ onBlur,
48
59
  id,
49
60
  name,
50
61
  register,
@@ -90,7 +101,7 @@ const InputDateTrigger = forwardRef<
90
101
  triggerInputRef.current?.click();
91
102
  };
92
103
 
93
- // 변경: Date trigger는 기본적으로 직접 타이핑을 막고, 달력 선택을 단일 입력 경로로 유지한다.
104
+ // Date trigger는 직접 타이핑을 막아 달력 선택을 단일 입력 경로로 유지한다.
94
105
  const shouldBlockTyping = !disabled && !readOnly;
95
106
 
96
107
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
@@ -170,9 +181,14 @@ const InputDateTrigger = forwardRef<
170
181
  name={name ?? register?.name}
171
182
  register={register}
172
183
  priority={priority}
184
+ size={size}
173
185
  state={state}
186
+ block={block}
187
+ width={width}
174
188
  placeholder={placeholder}
175
189
  className={clsx("input-date-trigger-input", className)}
190
+ onFocus={onFocus}
191
+ onBlur={onBlur}
176
192
  onClick={handleInputClick}
177
193
  onKeyDown={handleKeyDown}
178
194
  onBeforeInput={handleBeforeInput}