@uniai-fe/uds-primitives 0.1.13 → 0.2.0
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/README.md +2 -2
- package/dist/styles.css +1112 -385
- package/package.json +12 -15
- package/src/components/button/index.scss +1 -0
- package/src/components/button/markup/{ButtonRounded.tsx → Rounded.tsx} +1 -1
- package/src/components/button/markup/{ButtonText.tsx → Text.tsx} +1 -1
- package/src/components/button/markup/index.ts +3 -3
- package/src/components/button/styles/button.scss +113 -229
- package/src/components/button/styles/round-button.scss +11 -14
- package/src/components/button/styles/text-button.scss +23 -23
- package/src/components/button/styles/variables.scss +145 -0
- package/src/components/dropdown/index.tsx +3 -3
- package/src/components/dropdown/markup/Template.tsx +61 -0
- package/src/components/dropdown/markup/foundation/Container.tsx +97 -0
- package/src/components/dropdown/markup/foundation/MenuItem.tsx +107 -0
- package/src/components/dropdown/markup/foundation/MenuList.tsx +27 -0
- package/src/components/dropdown/markup/foundation/Provider.tsx +46 -0
- package/src/components/dropdown/markup/foundation/Root.tsx +30 -0
- package/src/components/dropdown/markup/foundation/Trigger.tsx +34 -0
- package/src/components/dropdown/markup/foundation/index.tsx +25 -0
- package/src/components/dropdown/markup/index.tsx +8 -2
- package/src/components/dropdown/styles/dropdown.scss +166 -0
- package/src/components/dropdown/styles/index.scss +2 -0
- package/src/components/dropdown/styles/variables.scss +40 -0
- package/src/components/dropdown/types/base.ts +18 -0
- package/src/components/dropdown/types/index.ts +2 -4
- package/src/components/dropdown/types/props.ts +174 -0
- package/src/components/dropdown/utils/index.ts +1 -4
- package/src/components/dropdown/utils/refs.ts +20 -0
- package/src/components/form/index.scss +1 -0
- package/src/components/form/index.tsx +18 -2
- package/src/components/form/markup/form-field/Body.tsx +18 -0
- package/src/components/form/markup/form-field/Container.tsx +58 -0
- package/src/components/form/markup/form-field/Footer.tsx +21 -0
- package/src/components/form/markup/form-field/Header.tsx +39 -0
- package/src/components/form/markup/form-field/Template.tsx +56 -0
- package/src/components/form/markup/form-field/index.tsx +22 -0
- package/src/components/form/styles/form-field/layout.scss +67 -0
- package/src/components/form/styles/form-field/variables.scss +17 -0
- package/src/components/form/styles/index.scss +2 -0
- package/src/components/form/types/index.ts +1 -0
- package/src/components/form/types/props.ts +125 -0
- package/src/components/form/utils/form-field.ts +42 -0
- package/src/components/input/hooks/index.ts +1 -4
- package/src/components/input/hooks/useDigitField.ts +63 -0
- package/src/components/input/img/calendar/calendar.svg +7 -0
- package/src/components/input/img/calendar/chevron-down.svg +3 -0
- package/src/components/input/img/calendar/chevron-left.svg +3 -0
- package/src/components/input/img/calendar/chevron-right.svg +3 -0
- package/src/components/input/img/calendar/chevron-up.svg +3 -0
- package/src/components/input/index.tsx +2 -1
- package/src/components/input/markup/calendar/Base.tsx +329 -0
- package/src/components/input/markup/calendar/index.tsx +8 -0
- package/src/components/input/markup/{text/InputUtilityButton.tsx → foundation/Button.tsx} +5 -15
- package/src/components/input/markup/foundation/Input.tsx +245 -0
- package/src/components/input/markup/foundation/SideSlot.tsx +30 -0
- package/src/components/input/markup/foundation/StatusIcon.tsx +21 -0
- package/src/components/input/markup/foundation/Utility.tsx +103 -0
- package/src/components/input/markup/foundation/index.tsx +15 -0
- package/src/components/input/markup/index.tsx +11 -1
- package/src/components/input/markup/text/AuthCode.tsx +41 -59
- package/src/components/input/markup/text/Email.tsx +25 -115
- package/src/components/input/markup/text/Password.tsx +30 -39
- package/src/components/input/markup/text/Phone.tsx +35 -122
- package/src/components/input/markup/text/Search.tsx +17 -18
- package/src/components/input/markup/text/index.ts +15 -12
- package/src/components/input/styles/calendar.scss +110 -0
- package/src/components/input/styles/foundation.scss +345 -0
- package/src/components/input/styles/index.scss +4 -476
- package/src/components/input/styles/text.scss +89 -0
- package/src/components/input/styles/variables.scss +41 -0
- package/src/components/input/types/calendar.ts +208 -0
- package/src/components/input/types/foundation.ts +194 -0
- package/src/components/input/types/hooks.ts +43 -0
- package/src/components/input/types/index.ts +5 -87
- package/src/components/input/types/text.ts +203 -0
- package/src/components/input/types/verification.ts +23 -0
- package/src/components/input/utils/index.tsx +1 -0
- package/src/components/input/utils/verification.tsx +35 -0
- package/src/components/select/hooks/index.ts +43 -2
- package/src/components/select/img/chevron/primary/large.svg +3 -0
- package/src/components/select/img/chevron/primary/medium.svg +3 -0
- package/src/components/select/img/chevron/primary/small.svg +3 -0
- package/src/components/select/img/chevron/secondary/large.svg +3 -0
- package/src/components/select/img/chevron/secondary/medium.svg +3 -0
- package/src/components/select/img/chevron/secondary/small.svg +3 -0
- package/src/components/select/img/remove.svg +3 -0
- package/src/components/select/index.scss +2 -1
- package/src/components/select/index.tsx +5 -0
- package/src/components/select/markup/Default.tsx +154 -0
- package/src/components/select/markup/foundation/Base.tsx +90 -0
- package/src/components/select/markup/foundation/Container.tsx +30 -0
- package/src/components/select/markup/foundation/Icon.tsx +78 -0
- package/src/components/select/markup/foundation/Selected.tsx +34 -0
- package/src/components/select/markup/foundation/index.ts +2 -0
- package/src/components/select/markup/index.tsx +36 -2
- package/src/components/select/markup/multiple/Multiple.tsx +205 -0
- package/src/components/select/markup/multiple/SelectedChip.tsx +58 -0
- package/src/components/select/markup/multiple/index.ts +2 -0
- package/src/components/select/styles/select.scss +316 -0
- package/src/components/select/styles/variables.scss +91 -0
- package/src/components/select/types/base.ts +34 -0
- package/src/components/select/types/icon.ts +45 -0
- package/src/components/select/types/index.ts +5 -4
- package/src/components/select/types/multiple.ts +57 -0
- package/src/components/select/types/props.ts +208 -0
- package/src/components/select/types/trigger.ts +196 -0
- package/src/index.scss +3 -2
- package/src/components/input/markup/text/Base.tsx +0 -454
- package/src/components/input/utils/index.ts +0 -60
- package/src/components/select/styles/index.scss +0 -0
- /package/src/components/button/markup/{ButtonDefault.tsx → Base.tsx} +0 -0
- /package/src/components/form/{Provider.tsx → markup/Provider.tsx} +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import type { FormFieldHeaderProps } from "../../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Form; field header
|
|
8
|
+
* @component
|
|
9
|
+
* @param {FormFieldHeaderProps} props
|
|
10
|
+
* @property {string} [className]
|
|
11
|
+
* @property {string} [label] 필드명
|
|
12
|
+
* @property {React.ElementType} [labelAs] 필드 태그 지정 (default: <h5 />)
|
|
13
|
+
* @property {string} [labelId] 필드 아이디 지정
|
|
14
|
+
* @property {boolean} [required] 필수 여부
|
|
15
|
+
* @property {React.ReactNode} [children] 추가 헤더 내용
|
|
16
|
+
* @property {React.HTMLAttributes<HTMLElement>} [labelProps] 추가 라벨 속성
|
|
17
|
+
*/
|
|
18
|
+
export default function FormFieldHeader({
|
|
19
|
+
className,
|
|
20
|
+
label,
|
|
21
|
+
labelAs: LabelTag = "h5",
|
|
22
|
+
labelId,
|
|
23
|
+
labelProps = {},
|
|
24
|
+
required,
|
|
25
|
+
children,
|
|
26
|
+
...headerAttrs
|
|
27
|
+
}: FormFieldHeaderProps) {
|
|
28
|
+
return (
|
|
29
|
+
<header className={clsx("form-field-header", className)} {...headerAttrs}>
|
|
30
|
+
{label && (
|
|
31
|
+
<LabelTag className="form-field-label" {...labelProps} id={labelId}>
|
|
32
|
+
<span>{label}</span>
|
|
33
|
+
{required && <span className="form-field-required">*</span>}
|
|
34
|
+
</LabelTag>
|
|
35
|
+
)}
|
|
36
|
+
{children}
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
|
|
5
|
+
import type { FormFieldTemplateProps } from "../../types/props";
|
|
6
|
+
import FormFieldHeader from "./Header";
|
|
7
|
+
import FormFieldBody from "./Body";
|
|
8
|
+
import FormFieldFooter from "./Footer";
|
|
9
|
+
import FormFieldContainer from "./Container";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Form; field template
|
|
13
|
+
* @component
|
|
14
|
+
* @param {FormFieldTemplateProps} props
|
|
15
|
+
* @property {string} [className]
|
|
16
|
+
* @property {React.ElementType} [as] container 태그 지정 (default: <section />)
|
|
17
|
+
* @property {React.ReactNode} [footer] 푸터 내용
|
|
18
|
+
* @property {FormFieldHeaderProps} [headerProps] 헤더 속성
|
|
19
|
+
* @property {FormFieldFooterProps} [footerProps] 푸터 속성
|
|
20
|
+
* @property {FormFieldWidth} [width] form field 너비 옵션
|
|
21
|
+
* @property {React.HTMLAttributes<HTMLElement>} [containerProps] container 속성
|
|
22
|
+
* @property {React.ReactNode} children
|
|
23
|
+
*/
|
|
24
|
+
const FormFieldTemplate = forwardRef<HTMLElement, FormFieldTemplateProps>(
|
|
25
|
+
(
|
|
26
|
+
{
|
|
27
|
+
className,
|
|
28
|
+
as,
|
|
29
|
+
footer,
|
|
30
|
+
headerProps,
|
|
31
|
+
footerProps,
|
|
32
|
+
children,
|
|
33
|
+
width,
|
|
34
|
+
...containerProps
|
|
35
|
+
},
|
|
36
|
+
forwardedRef,
|
|
37
|
+
) => {
|
|
38
|
+
return (
|
|
39
|
+
<FormFieldContainer
|
|
40
|
+
ref={forwardedRef}
|
|
41
|
+
as={as}
|
|
42
|
+
className={className}
|
|
43
|
+
width={width}
|
|
44
|
+
{...containerProps}
|
|
45
|
+
>
|
|
46
|
+
<FormFieldHeader {...headerProps} />
|
|
47
|
+
<FormFieldBody>{children}</FormFieldBody>
|
|
48
|
+
{footer && <FormFieldFooter {...footerProps}>{footer}</FormFieldFooter>}
|
|
49
|
+
</FormFieldContainer>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
FormFieldTemplate.displayName = "FormFieldTemplate";
|
|
55
|
+
|
|
56
|
+
export default FormFieldTemplate;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import FormFieldContainer from "./Container";
|
|
2
|
+
import FormFieldHeader from "./Header";
|
|
3
|
+
import FormFieldBody from "./Body";
|
|
4
|
+
import FormFieldFooter from "./Footer";
|
|
5
|
+
import FormFieldTemplate from "./Template";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Form; form field 컴포넌트
|
|
9
|
+
* @component
|
|
10
|
+
* @desc
|
|
11
|
+
* - FormField.Container: Form 필드의 컨테이너 컴포넌트
|
|
12
|
+
* - FormField.Header: Form 필드의 헤더 컴포넌트
|
|
13
|
+
* - FormField.Body: Form 필드의 본문 컴포넌트
|
|
14
|
+
* - FormField.Footer: Form 필드의 푸터 컴포넌트
|
|
15
|
+
*/
|
|
16
|
+
export const FormField = {
|
|
17
|
+
Container: FormFieldContainer,
|
|
18
|
+
Header: FormFieldHeader,
|
|
19
|
+
Body: FormFieldBody,
|
|
20
|
+
Footer: FormFieldFooter,
|
|
21
|
+
Template: FormFieldTemplate,
|
|
22
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
.form-field-container {
|
|
2
|
+
display: block;
|
|
3
|
+
width: var(--form-field-width);
|
|
4
|
+
flex: var(--form-field-flex);
|
|
5
|
+
|
|
6
|
+
&[data-width="auto"] {
|
|
7
|
+
--form-field-width: auto;
|
|
8
|
+
--form-field-flex: 0 0 auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
&[data-width="fill"] {
|
|
12
|
+
--form-field-width: auto;
|
|
13
|
+
--form-field-flex: 1 1 0%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
&[data-width="full"] {
|
|
17
|
+
--form-field-width: 100%;
|
|
18
|
+
--form-field-flex: 0 0 100%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&[data-width="fit"] {
|
|
22
|
+
--form-field-width: fit-content;
|
|
23
|
+
--form-field-flex: 0 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&[data-width="custom"] {
|
|
27
|
+
--form-field-flex: 0 0 auto;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.form-field-header {
|
|
32
|
+
margin-bottom: var(--form-field-gap-y);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.form-field-label {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: var(--spacing-gap-1);
|
|
39
|
+
|
|
40
|
+
font-size: var(--form-field-label-font-size);
|
|
41
|
+
font-weight: var(--form-field-label-font-weight);
|
|
42
|
+
line-height: var(--form-field-label-line-height);
|
|
43
|
+
span {
|
|
44
|
+
color: var(--form-field-label-color);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.form-field-required {
|
|
49
|
+
color: var(--color-feedback-error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.form-field-body {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
gap: var(--form-field-gap-x);
|
|
56
|
+
width: 100%;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.form-field-footer {
|
|
60
|
+
margin-top: var(--form-field-gap-y);
|
|
61
|
+
font-size: var(--form-field-helper-font-size);
|
|
62
|
+
line-height: var(--form-field-helper-line-height);
|
|
63
|
+
p,
|
|
64
|
+
span {
|
|
65
|
+
color: var(--form-field-helper-color);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--form-field-width: auto;
|
|
3
|
+
--form-field-flex: 0 0 auto;
|
|
4
|
+
|
|
5
|
+
--form-field-gap-x: var(--spacing-gap-5, 12px);
|
|
6
|
+
--form-field-gap-y: var(--spacing-gap-3, 6px);
|
|
7
|
+
|
|
8
|
+
--form-field-label-font-size: var(--font-label-small-size);
|
|
9
|
+
--form-field-label-font-weight: 400;
|
|
10
|
+
--form-field-label-line-height: var(--font-label-small-line-height);
|
|
11
|
+
--form-field-label-color: var(--color-label-standard);
|
|
12
|
+
|
|
13
|
+
--form-field-helper-font-size: var(--font-caption-medium-size);
|
|
14
|
+
--form-field-helper-font-weight: 400;
|
|
15
|
+
--form-field-helper-line-height: var(--font-caption-medium-line-height);
|
|
16
|
+
--form-field-helper-color: var(--color-label-neutral);
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type * from "./props";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ElementType } from "react";
|
|
2
|
+
|
|
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 문자열
|
|
11
|
+
*/
|
|
12
|
+
export type FormFieldWidth = "full" | "fit" | "fill" | "auto" | number | string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Form.Field Header props; `Header.tsx`에서 label/description/meta를 제어한다.
|
|
16
|
+
* @property {string} [className] 헤더 wrapper className
|
|
17
|
+
* @property {React.ElementType} [labelAs] heading 태그 지정 (default: h5)
|
|
18
|
+
* @property {string} [labelId] label DOM id
|
|
19
|
+
* @property {string} [label] 기본 label 텍스트
|
|
20
|
+
* @property {React.ReactNode} [labelJsx] label JSX 대체
|
|
21
|
+
* @property {React.HTMLAttributes<HTMLElement>} [labelProps] label 태그 속성
|
|
22
|
+
* @property {boolean} [required] 필수 표시
|
|
23
|
+
* @property {React.ReactNode} [children] description 등 추가 요소
|
|
24
|
+
*/
|
|
25
|
+
export interface FormFieldHeaderProps extends React.HTMLAttributes<HTMLElement> {
|
|
26
|
+
/**
|
|
27
|
+
* 헤더 wrapper className
|
|
28
|
+
*/
|
|
29
|
+
className?: string;
|
|
30
|
+
/**
|
|
31
|
+
* label heading 태그
|
|
32
|
+
*/
|
|
33
|
+
labelAs?: ElementType;
|
|
34
|
+
/**
|
|
35
|
+
* label DOM id
|
|
36
|
+
*/
|
|
37
|
+
labelId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* 기본 label 텍스트
|
|
40
|
+
*/
|
|
41
|
+
label?: string;
|
|
42
|
+
/**
|
|
43
|
+
* 커스텀 label JSX
|
|
44
|
+
*/
|
|
45
|
+
labelJsx?: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* label 태그 추가 속성
|
|
48
|
+
*/
|
|
49
|
+
labelProps?: React.HTMLAttributes<HTMLElement>;
|
|
50
|
+
/**
|
|
51
|
+
* 필수 여부
|
|
52
|
+
*/
|
|
53
|
+
required?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* description 등 추가 요소
|
|
56
|
+
*/
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Form.Field Footer props; `Footer.tsx`에서 helper/action 영역을 담당한다.
|
|
62
|
+
* @property {string} [className] footer className
|
|
63
|
+
* @property {React.ReactNode} [children] footer 내용
|
|
64
|
+
*/
|
|
65
|
+
export interface FormFieldFooterProps extends React.HTMLAttributes<HTMLElement> {
|
|
66
|
+
/**
|
|
67
|
+
* footer className
|
|
68
|
+
*/
|
|
69
|
+
className?: string;
|
|
70
|
+
/**
|
|
71
|
+
* footer 내용
|
|
72
|
+
*/
|
|
73
|
+
children?: React.ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Form.Field Container props; `Container.tsx`에서 wrapper와 width 옵션을 제어한다.
|
|
78
|
+
* @property {string} [className] container className
|
|
79
|
+
* @property {React.ElementType} [as] container 태그
|
|
80
|
+
* @property {FormFieldWidth} [width] width 옵션(full/fit/fill/auto/custom)
|
|
81
|
+
* @property {React.ReactNode} children Body에 전달될 콘텐츠
|
|
82
|
+
*/
|
|
83
|
+
export interface FormFieldContainerProps extends React.HTMLAttributes<HTMLElement> {
|
|
84
|
+
/**
|
|
85
|
+
* container className
|
|
86
|
+
*/
|
|
87
|
+
className?: string;
|
|
88
|
+
/**
|
|
89
|
+
* container 태그
|
|
90
|
+
*/
|
|
91
|
+
as?: ElementType;
|
|
92
|
+
/**
|
|
93
|
+
* width 옵션
|
|
94
|
+
*/
|
|
95
|
+
width?: FormFieldWidth;
|
|
96
|
+
/**
|
|
97
|
+
* 필드 children
|
|
98
|
+
*/
|
|
99
|
+
children: React.ReactNode;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Form.Field Template props; `Template.tsx`에서 Header/Footer 조합을 제공한다.
|
|
104
|
+
* @property {React.ReactNode} [footer] Footer helper 내용
|
|
105
|
+
* @property {FormFieldHeaderProps} [headerProps] Header props
|
|
106
|
+
* @property {FormFieldFooterProps} [footerProps] Footer props
|
|
107
|
+
* @property {string} [className] container className
|
|
108
|
+
* @property {React.ElementType} [as] container 태그
|
|
109
|
+
* @property {FormFieldWidth} [width] width 옵션(full/fit/fill/auto/custom)
|
|
110
|
+
* @property {React.ReactNode} children Body에 전달될 콘텐츠
|
|
111
|
+
*/
|
|
112
|
+
export interface FormFieldTemplateProps extends FormFieldContainerProps {
|
|
113
|
+
/**
|
|
114
|
+
* Footer helper 내용
|
|
115
|
+
*/
|
|
116
|
+
footer?: React.ReactNode;
|
|
117
|
+
/**
|
|
118
|
+
* Header props
|
|
119
|
+
*/
|
|
120
|
+
headerProps?: FormFieldHeaderProps;
|
|
121
|
+
/**
|
|
122
|
+
* Footer props
|
|
123
|
+
*/
|
|
124
|
+
footerProps?: FormFieldFooterProps;
|
|
125
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { styleBaseSize } from "@uniai-fe/util-functions";
|
|
2
|
+
import type { FormFieldWidth } from "../types";
|
|
3
|
+
|
|
4
|
+
const WIDTH_PRESETS = new Set<FormFieldWidth>(["full", "fit", "fill", "auto"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Form.Field width attr helper; preset이면 그대로 반환하고 custom이면 "custom"/기본값은 "auto".
|
|
8
|
+
* @function
|
|
9
|
+
* @param {FormFieldWidth} [width]
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
export const getFormFieldWidthAttr = (width?: FormFieldWidth): string => {
|
|
13
|
+
if (typeof width === "string" && WIDTH_PRESETS.has(width)) {
|
|
14
|
+
return width;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof width === "number" || typeof width === "string") {
|
|
18
|
+
return "custom";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return "auto";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Form.Field width style helper; number/string custom 값을 CSS width 문자열로 환산한다.
|
|
26
|
+
* @function
|
|
27
|
+
* @param {FormFieldWidth} [width]
|
|
28
|
+
* @returns {string | undefined}
|
|
29
|
+
*/
|
|
30
|
+
export const getFormFieldWidthValue = (
|
|
31
|
+
width?: FormFieldWidth,
|
|
32
|
+
): string | undefined => {
|
|
33
|
+
if (typeof width === "number" && Number.isFinite(width)) {
|
|
34
|
+
return styleBaseSize("rem", width);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof width === "string" && !WIDTH_PRESETS.has(width)) {
|
|
38
|
+
return width;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import type { UseDigitFieldOptions, UseDigitFieldResult } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 숫자 입력값만 유지하도록 정규화한다.
|
|
8
|
+
* @param {string | undefined} value 원본 문자열
|
|
9
|
+
* @returns {string} 숫자만 남긴 문자열
|
|
10
|
+
*/
|
|
11
|
+
export const normalizeDigits = (value?: string) =>
|
|
12
|
+
(value ?? "").replace(/\D/g, "");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 숫자 기반 입력 필드의 제어/비제어 값을 일관되게 다룬다.
|
|
16
|
+
* @hook
|
|
17
|
+
* @param {UseDigitFieldOptions} options value/defaultValue/maxLength 옵션
|
|
18
|
+
*/
|
|
19
|
+
export const useDigitField = ({
|
|
20
|
+
value,
|
|
21
|
+
defaultValue,
|
|
22
|
+
maxLength,
|
|
23
|
+
}: UseDigitFieldOptions): UseDigitFieldResult => {
|
|
24
|
+
const limit = maxLength ?? Number.POSITIVE_INFINITY;
|
|
25
|
+
const isControlled = value !== undefined;
|
|
26
|
+
const clampDigits = useCallback(
|
|
27
|
+
(raw: string) => normalizeDigits(raw).slice(0, limit),
|
|
28
|
+
[limit],
|
|
29
|
+
);
|
|
30
|
+
const [innerDigits, setInnerDigits] = useState(() =>
|
|
31
|
+
clampDigits(defaultValue ?? ""),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// uncontrolled 값이 있고 maxLength가 줄어들면 즉시 잘라낸다.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isControlled) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
setInnerDigits(prev => clampDigits(prev));
|
|
40
|
+
}, [clampDigits, isControlled]);
|
|
41
|
+
|
|
42
|
+
const resolvedDigits = useMemo(() => {
|
|
43
|
+
const target = isControlled ? (value ?? "") : innerDigits;
|
|
44
|
+
return clampDigits(target);
|
|
45
|
+
}, [clampDigits, innerDigits, isControlled, value]);
|
|
46
|
+
|
|
47
|
+
const handleDigitsChange = useCallback(
|
|
48
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
49
|
+
const digitsOnly = clampDigits(event.currentTarget.value);
|
|
50
|
+
if (!isControlled) {
|
|
51
|
+
setInnerDigits(digitsOnly);
|
|
52
|
+
}
|
|
53
|
+
return digitsOnly;
|
|
54
|
+
},
|
|
55
|
+
[clampDigits, isControlled],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
digits: resolvedDigits,
|
|
60
|
+
isControlled,
|
|
61
|
+
handleDigitsChange,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect x="20.2" y="6.8" width="13.4" height="16.4" rx="1.2" transform="rotate(90 20.2 6.8)" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
3
|
+
<path d="M4 10C9.33333 10 20 10 20 10" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
4
|
+
<path d="M7 13L9 13" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
5
|
+
<path d="M8.8 4C8.8 3.55817 8.44183 3.2 8 3.2C7.55817 3.2 7.2 3.55817 7.2 4L8 4L8.8 4ZM8 4L7.2 4L7.2 6L8 6L8.8 6L8.8 4L8 4Z" fill="#BCBEC2"/>
|
|
6
|
+
<path d="M16.8 4C16.8 3.55817 16.4418 3.2 16 3.2C15.5582 3.2 15.2 3.55817 15.2 4L16 4L16.8 4ZM16 4L15.2 4L15.2 6L16 6L16.8 6L16.8 4L16 4Z" fill="#BCBEC2"/>
|
|
7
|
+
</svg>
|