@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,203 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from "react";
|
|
2
|
+
import type { ButtonPriority } from "../../button/types";
|
|
3
|
+
import type { InputProps } from "./foundation";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 입력 유틸리티 버튼 클릭 이벤트 타입.
|
|
7
|
+
*/
|
|
8
|
+
export type InputUtilityButtonClickHandler = (
|
|
9
|
+
event?: MouseEvent<HTMLButtonElement> | MouseEvent<HTMLAnchorElement>,
|
|
10
|
+
) => void;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 입력 유틸리티 버튼 props 정의.
|
|
14
|
+
* @property {ReactNode} children 버튼 라벨
|
|
15
|
+
* @property {ButtonPriority} [priority] 버튼 우선순위
|
|
16
|
+
* @property {boolean} [disabled] 버튼 disabled 상태
|
|
17
|
+
* @property {InputUtilityButtonClickHandler} [onClick] 클릭 핸들러
|
|
18
|
+
*/
|
|
19
|
+
export interface InputUtilityButtonProps {
|
|
20
|
+
/**
|
|
21
|
+
* 버튼 라벨
|
|
22
|
+
*/
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* 버튼 우선순위
|
|
26
|
+
*/
|
|
27
|
+
priority?: ButtonPriority;
|
|
28
|
+
/**
|
|
29
|
+
* disabled 여부
|
|
30
|
+
*/
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* 클릭 핸들러
|
|
34
|
+
*/
|
|
35
|
+
onClick?: InputUtilityButtonClickHandler;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Search Input props; type 만 search 로 고정한다.
|
|
40
|
+
*/
|
|
41
|
+
export interface SearchInputProps extends Omit<InputProps, "type"> {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Password Input props; 보기/숨김 토글 옵션 포함.
|
|
45
|
+
* @property {boolean} [defaultVisible=false] 초기 표시 여부
|
|
46
|
+
* @property {{show:string; hide:string}} [toggleLabel] 토글 버튼 라벨 세트
|
|
47
|
+
*/
|
|
48
|
+
export interface InputPasswordProps extends Omit<InputProps, "type"> {
|
|
49
|
+
/**
|
|
50
|
+
* 초기 표시 여부
|
|
51
|
+
*/
|
|
52
|
+
defaultVisible?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* 토글 버튼 라벨 세트
|
|
55
|
+
*/
|
|
56
|
+
toggleLabel?: {
|
|
57
|
+
show: string;
|
|
58
|
+
hide: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* AuthCode Input props; 자리수/카운트다운 옵션을 담는다.
|
|
64
|
+
* @property {string} [value] 제어형 값
|
|
65
|
+
* @property {string} [defaultValue] 비제어 초기값
|
|
66
|
+
* @property {(value:string)=>void} [onValueChange] 값 변경 시 호출
|
|
67
|
+
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override
|
|
68
|
+
* @property {number} [length] 자리수 제한
|
|
69
|
+
* @property {(code:string)=>void} [onComplete] 입력 완료 콜백
|
|
70
|
+
* @property {string} [placeholder] placeholder 텍스트
|
|
71
|
+
* @property {ReactNode} [countdownText] 제한 시간 안내
|
|
72
|
+
* @property {ReactNode} [countdownActionLabel] 연장 버튼 라벨
|
|
73
|
+
* @property {InputUtilityButtonClickHandler} [onCountdownAction] 연장 버튼 클릭 시 호출
|
|
74
|
+
* @property {boolean} [countdownActionDisabled] 연장 버튼 disabled 여부
|
|
75
|
+
*/
|
|
76
|
+
export interface AuthCodeInputProps extends Omit<
|
|
77
|
+
InputProps,
|
|
78
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
79
|
+
> {
|
|
80
|
+
/**
|
|
81
|
+
* 제어형 코드 값
|
|
82
|
+
*/
|
|
83
|
+
value?: string;
|
|
84
|
+
/**
|
|
85
|
+
* 비제어 초기값
|
|
86
|
+
*/
|
|
87
|
+
defaultValue?: string;
|
|
88
|
+
/**
|
|
89
|
+
* 값 변경 콜백
|
|
90
|
+
*/
|
|
91
|
+
onValueChange?: (value: string) => void;
|
|
92
|
+
/**
|
|
93
|
+
* native onChange override
|
|
94
|
+
*/
|
|
95
|
+
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
96
|
+
/**
|
|
97
|
+
* 자리수 제한
|
|
98
|
+
*/
|
|
99
|
+
length?: number;
|
|
100
|
+
/**
|
|
101
|
+
* 입력 완료 콜백
|
|
102
|
+
*/
|
|
103
|
+
onComplete?: (code: string) => void;
|
|
104
|
+
/**
|
|
105
|
+
* placeholder 텍스트
|
|
106
|
+
*/
|
|
107
|
+
placeholder?: string;
|
|
108
|
+
/**
|
|
109
|
+
* 제한 시간 안내 텍스트
|
|
110
|
+
*/
|
|
111
|
+
countdownText?: ReactNode;
|
|
112
|
+
/**
|
|
113
|
+
* 연장 버튼 라벨
|
|
114
|
+
*/
|
|
115
|
+
countdownActionLabel?: ReactNode;
|
|
116
|
+
/**
|
|
117
|
+
* 연장 버튼 클릭 핸들러
|
|
118
|
+
*/
|
|
119
|
+
onCountdownAction?: InputUtilityButtonClickHandler;
|
|
120
|
+
/**
|
|
121
|
+
* 연장 버튼 disabled 여부
|
|
122
|
+
*/
|
|
123
|
+
countdownActionDisabled?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Email Input props; 기본 이메일 입력과 인증요청 버튼만을 담당한다.
|
|
128
|
+
* @property {InputUtilityButtonClickHandler} [onRequestCode] 인증 요청 클릭 핸들러
|
|
129
|
+
* @property {ReactNode} [requestButtonLabel] 인증 요청 버튼 라벨
|
|
130
|
+
* @property {boolean} [requestButtonDisabled] 인증 요청 disabled
|
|
131
|
+
*/
|
|
132
|
+
export interface EmailInputProps extends Omit<
|
|
133
|
+
InputProps,
|
|
134
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
135
|
+
> {
|
|
136
|
+
/**
|
|
137
|
+
* 제어형 이메일 값
|
|
138
|
+
*/
|
|
139
|
+
value?: string;
|
|
140
|
+
/**
|
|
141
|
+
* 비제어 초기값
|
|
142
|
+
*/
|
|
143
|
+
defaultValue?: string;
|
|
144
|
+
/**
|
|
145
|
+
* 값 변경 콜백
|
|
146
|
+
*/
|
|
147
|
+
onValueChange?: (value: string) => void;
|
|
148
|
+
/**
|
|
149
|
+
* native onChange override
|
|
150
|
+
*/
|
|
151
|
+
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
152
|
+
/**
|
|
153
|
+
* 인증 요청 클릭 핸들러
|
|
154
|
+
*/
|
|
155
|
+
onRequestCode?: InputUtilityButtonClickHandler;
|
|
156
|
+
/**
|
|
157
|
+
* 인증 요청 버튼 라벨
|
|
158
|
+
*/
|
|
159
|
+
requestButtonLabel?: ReactNode;
|
|
160
|
+
/**
|
|
161
|
+
* 인증 요청 버튼 disabled
|
|
162
|
+
*/
|
|
163
|
+
requestButtonDisabled?: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Phone Input props; 전화번호 포맷팅과 인증요청 버튼만 담당한다.
|
|
168
|
+
* @property {(value:string,digits:string)=>void} [onValueChange] 포맷/숫자 콜백
|
|
169
|
+
* 나머지 props는 InputProps 기반이며 type 관련 속성을 제외한다.
|
|
170
|
+
*/
|
|
171
|
+
export interface PhoneInputProps extends Omit<
|
|
172
|
+
InputProps,
|
|
173
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
174
|
+
> {
|
|
175
|
+
/**
|
|
176
|
+
* 제어형 포맷 값
|
|
177
|
+
*/
|
|
178
|
+
value?: string;
|
|
179
|
+
/**
|
|
180
|
+
* 비제어 초기값
|
|
181
|
+
*/
|
|
182
|
+
defaultValue?: string;
|
|
183
|
+
/**
|
|
184
|
+
* 포맷/숫자 콜백
|
|
185
|
+
*/
|
|
186
|
+
onValueChange?: (value: string, digits: string) => void;
|
|
187
|
+
/**
|
|
188
|
+
* native onChange override
|
|
189
|
+
*/
|
|
190
|
+
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
191
|
+
/**
|
|
192
|
+
* 인증 요청 핸들러
|
|
193
|
+
*/
|
|
194
|
+
onRequestCode?: InputUtilityButtonClickHandler;
|
|
195
|
+
/**
|
|
196
|
+
* 인증 요청 라벨
|
|
197
|
+
*/
|
|
198
|
+
requestButtonLabel?: ReactNode;
|
|
199
|
+
/**
|
|
200
|
+
* 인증 요청 disabled
|
|
201
|
+
*/
|
|
202
|
+
requestButtonDisabled?: boolean;
|
|
203
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { InputUtilityButtonClickHandler } from "./text";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 인증 요청 버튼 옵션.
|
|
6
|
+
* @property {InputUtilityButtonClickHandler} [onRequestCode] 클릭 핸들러
|
|
7
|
+
* @property {ReactNode} [requestButtonLabel] 버튼 라벨
|
|
8
|
+
* @property {boolean} [requestButtonDisabled] disabled 여부
|
|
9
|
+
*/
|
|
10
|
+
export interface VerificationRequestButtonOptions {
|
|
11
|
+
/**
|
|
12
|
+
* 인증 요청 클릭 핸들러
|
|
13
|
+
*/
|
|
14
|
+
onRequestCode?: InputUtilityButtonClickHandler;
|
|
15
|
+
/**
|
|
16
|
+
* 버튼 라벨
|
|
17
|
+
*/
|
|
18
|
+
requestButtonLabel?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* 버튼 disabled 상태
|
|
21
|
+
*/
|
|
22
|
+
requestButtonDisabled?: boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./verification";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import InputBaseUtilityButton from "../markup/foundation/Button";
|
|
4
|
+
import type { VerificationRequestButtonOptions } from "../types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 인증 요청 버튼의 기본 라벨.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_REQUEST_LABEL = "인증번호 요청";
|
|
10
|
+
/**
|
|
11
|
+
* 인증 요청 버튼을 렌더링한다.
|
|
12
|
+
* @function
|
|
13
|
+
* @param {VerificationRequestButtonOptions} options 버튼 옵션
|
|
14
|
+
* @returns {React.ReactNode} 버튼 노드
|
|
15
|
+
*/
|
|
16
|
+
export const renderVerificationRequestButton = ({
|
|
17
|
+
onRequestCode,
|
|
18
|
+
requestButtonDisabled,
|
|
19
|
+
requestButtonLabel = DEFAULT_REQUEST_LABEL,
|
|
20
|
+
}: VerificationRequestButtonOptions): React.ReactNode => {
|
|
21
|
+
// 인증 요청 핸들러가 없으면 버튼 자체를 렌더링하지 않는다.
|
|
22
|
+
if (!onRequestCode) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<InputBaseUtilityButton
|
|
28
|
+
priority="primary"
|
|
29
|
+
onClick={onRequestCode}
|
|
30
|
+
disabled={requestButtonDisabled}
|
|
31
|
+
>
|
|
32
|
+
{requestButtonLabel}
|
|
33
|
+
</InputBaseUtilityButton>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
@@ -1,4 +1,45 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { SelectDropdownBehaviorProps } from "../types/props";
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
6
|
+
* Select dropdown open 상태를 제어하는 hook
|
|
7
|
+
* @hook
|
|
8
|
+
* @param {SelectDropdownBehaviorProps} props open 제어 옵션
|
|
9
|
+
* @returns {{
|
|
10
|
+
* open: boolean;
|
|
11
|
+
* setOpen: (next: boolean) => void;
|
|
12
|
+
* isControlled: boolean;
|
|
13
|
+
* }} resolved open state
|
|
3
14
|
*/
|
|
4
|
-
export {
|
|
15
|
+
export const useSelectDropdownOpenState = ({
|
|
16
|
+
open,
|
|
17
|
+
defaultOpen,
|
|
18
|
+
onOpenChange,
|
|
19
|
+
}: SelectDropdownBehaviorProps) => {
|
|
20
|
+
const isControlled = useMemo(() => typeof open === "boolean", [open]);
|
|
21
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(
|
|
22
|
+
defaultOpen ?? false,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (isControlled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setUncontrolledOpen(defaultOpen ?? false);
|
|
30
|
+
}, [defaultOpen, isControlled]);
|
|
31
|
+
|
|
32
|
+
const resolvedOpen = isControlled ? (open as boolean) : uncontrolledOpen;
|
|
33
|
+
|
|
34
|
+
const setOpen = useCallback(
|
|
35
|
+
(nextOpen: boolean) => {
|
|
36
|
+
if (!isControlled) {
|
|
37
|
+
setUncontrolledOpen(nextOpen);
|
|
38
|
+
}
|
|
39
|
+
onOpenChange?.(nextOpen);
|
|
40
|
+
},
|
|
41
|
+
[isControlled, onOpenChange],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return { open: resolvedOpen, setOpen, isControlled };
|
|
45
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M15.9592 6.79278L9.99992 12.7521L4.04061 6.79278C3.65009 6.40225 3.01669 6.40225 2.62617 6.79278C2.23564 7.1833 2.23564 7.8167 2.62617 8.20722L9.2927 14.8738C9.68322 15.2643 10.3166 15.2643 10.7071 14.8738L17.3737 8.20722C17.7642 7.8167 17.7642 7.1833 17.3737 6.79278C16.9831 6.40239 16.3497 6.4023 15.9592 6.79278Z" fill="#BCBEC2"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12.7674 5.43422L7.99994 10.2017L3.23249 5.43422C2.92007 5.1218 2.41335 5.1218 2.10093 5.43422C1.78851 5.74664 1.78851 6.25336 2.10093 6.56578L7.43416 11.899C7.74658 12.2114 8.25329 12.2114 8.56571 11.899L13.8989 6.56578C14.2114 6.25336 14.2114 5.74664 13.8989 5.43422C13.5865 5.12191 13.0798 5.12184 12.7674 5.43422Z" fill="#BCBEC2"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M9.57554 4.07567L5.99995 7.65125L2.42437 4.07567C2.19005 3.84135 1.81001 3.84135 1.5757 4.07567C1.34139 4.30998 1.34139 4.69002 1.5757 4.92433L5.57562 8.92425C5.80993 9.15857 6.18997 9.15857 6.42428 8.92425L10.4242 4.92433C10.6585 4.69002 10.6585 4.30998 10.4242 4.07567C10.1899 3.84143 9.80982 3.84138 9.57554 4.07567Z" fill="#BCBEC2"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M15.9592 6.79278L9.99992 12.7521L4.04061 6.79278C3.65009 6.40225 3.01669 6.40225 2.62617 6.79278C2.23564 7.1833 2.23564 7.8167 2.62617 8.20722L9.2927 14.8738C9.68322 15.2643 10.3166 15.2643 10.7071 14.8738L17.3737 8.20722C17.7642 7.8167 17.7642 7.1833 17.3737 6.79278C16.9831 6.40239 16.3497 6.4023 15.9592 6.79278Z" fill="#313235"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12.7674 5.43422L7.99994 10.2017L3.23249 5.43422C2.92007 5.1218 2.41335 5.1218 2.10093 5.43422C1.78851 5.74664 1.78851 6.25336 2.10093 6.56578L7.43416 11.899C7.74658 12.2114 8.25329 12.2114 8.56571 11.899L13.8989 6.56578C14.2114 6.25336 14.2114 5.74664 13.8989 5.43422C13.5865 5.12191 13.0798 5.12184 12.7674 5.43422Z" fill="#313235"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M9.57554 4.07567L5.99995 7.65125L2.42437 4.07567C2.19005 3.84135 1.81001 3.84135 1.5757 4.07567C1.34139 4.30998 1.34139 4.69002 1.5757 4.92433L5.57562 8.92425C5.80993 9.15857 6.18997 9.15857 6.42428 8.92425L10.4242 4.92433C10.6585 4.69002 10.6585 4.30998 10.4242 4.07567C10.1899 3.84143 9.80982 3.84138 9.57554 4.07567Z" fill="#313235"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.0267 5.97331C9.81841 5.76513 9.48103 5.76506 9.27279 5.97331L7.99935 7.24609L6.72656 5.97331C6.51827 5.76513 6.1809 5.76506 5.97266 5.97331C5.76468 6.18157 5.76457 6.51902 5.97266 6.72721L7.24544 8L5.97266 9.27279C5.76457 9.48099 5.76468 9.81843 5.97266 10.0267C6.1809 10.2349 6.51827 10.2349 6.72656 10.0267L7.99935 8.75326L9.27279 10.0267C9.48103 10.2349 9.81841 10.2349 10.0267 10.0267C10.2348 9.8184 10.2349 9.48101 10.0267 9.27279L8.75326 8L10.0267 6.72721C10.2349 6.51899 10.2348 6.1816 10.0267 5.97331Z" fill="#313235"/>
|
|
3
|
+
</svg>
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
@use "./styles/
|
|
1
|
+
@use "./styles/variables.scss";
|
|
2
|
+
@use "./styles/select.scss";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
import type { DropdownSize } from "../../dropdown/types";
|
|
7
|
+
import { Dropdown } from "../../dropdown/markup";
|
|
8
|
+
import { SelectTriggerBase, SelectTriggerSelected } from "./foundation";
|
|
9
|
+
import Container from "./foundation/Container";
|
|
10
|
+
import { useSelectDropdownOpenState } from "../hooks";
|
|
11
|
+
import type {
|
|
12
|
+
SelectDefaultComponentProps,
|
|
13
|
+
SelectDropdownOption,
|
|
14
|
+
} from "../types/props";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Select default trigger; 단일 선택 드롭다운을 렌더링한다.
|
|
18
|
+
* @component
|
|
19
|
+
* @param {SelectDefaultComponentProps} props default trigger props
|
|
20
|
+
* @param {SelectDropdownOption[]} [props.options] dropdown option 목록
|
|
21
|
+
* @param {string[]} [props.selectedOptionIds] 선택된 option id 리스트
|
|
22
|
+
* @param {(option: SelectDropdownOption) => void} [props.onOptionSelect] option 선택 콜백
|
|
23
|
+
* @param {"primary" | "secondary"} [props.priority="primary"] priority 스케일
|
|
24
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] size 스케일
|
|
25
|
+
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
26
|
+
* @param {boolean} [props.block] block 여부
|
|
27
|
+
* @param {boolean} [props.disabled] disabled 여부
|
|
28
|
+
*/
|
|
29
|
+
const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
|
|
30
|
+
(
|
|
31
|
+
{
|
|
32
|
+
className,
|
|
33
|
+
displayLabel,
|
|
34
|
+
placeholder,
|
|
35
|
+
priority = "primary",
|
|
36
|
+
size = "medium",
|
|
37
|
+
state = "default",
|
|
38
|
+
block,
|
|
39
|
+
isOpen,
|
|
40
|
+
disabled,
|
|
41
|
+
buttonType,
|
|
42
|
+
options = [],
|
|
43
|
+
selectedOptionIds,
|
|
44
|
+
onOptionSelect,
|
|
45
|
+
dropdownSize,
|
|
46
|
+
dropdownMatchTriggerWidth = true,
|
|
47
|
+
dropdownRootProps,
|
|
48
|
+
dropdownContainerProps,
|
|
49
|
+
dropdownMenuListProps,
|
|
50
|
+
open,
|
|
51
|
+
defaultOpen,
|
|
52
|
+
onOpenChange,
|
|
53
|
+
...rest
|
|
54
|
+
},
|
|
55
|
+
ref,
|
|
56
|
+
) => {
|
|
57
|
+
const resolvedSelectedIds = selectedOptionIds ?? [];
|
|
58
|
+
|
|
59
|
+
const resolvedDisplayLabel =
|
|
60
|
+
displayLabel ??
|
|
61
|
+
(resolvedSelectedIds.length > 0
|
|
62
|
+
? options.find(option => option.id === resolvedSelectedIds[0])?.label
|
|
63
|
+
: undefined);
|
|
64
|
+
|
|
65
|
+
const hasLabel =
|
|
66
|
+
resolvedDisplayLabel !== undefined &&
|
|
67
|
+
resolvedDisplayLabel !== null &&
|
|
68
|
+
resolvedDisplayLabel !== "";
|
|
69
|
+
|
|
70
|
+
const { open: dropdownOpen, setOpen } = useSelectDropdownOpenState({
|
|
71
|
+
open: open ?? isOpen,
|
|
72
|
+
defaultOpen,
|
|
73
|
+
onOpenChange,
|
|
74
|
+
});
|
|
75
|
+
// Dropdown open 상태를 trigger data-state와 동기화한다.
|
|
76
|
+
|
|
77
|
+
const handleOptionSelect = (option: SelectDropdownOption) => {
|
|
78
|
+
onOptionSelect?.(option);
|
|
79
|
+
setOpen(false);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const panelSize = (dropdownSize ?? size) as DropdownSize;
|
|
83
|
+
const shouldRenderDropdown = options.length > 0;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Container
|
|
87
|
+
className={clsx("select-trigger-container", className)}
|
|
88
|
+
block={block}
|
|
89
|
+
>
|
|
90
|
+
<Dropdown.Root
|
|
91
|
+
open={dropdownOpen}
|
|
92
|
+
onOpenChange={setOpen}
|
|
93
|
+
modal={false}
|
|
94
|
+
{...dropdownRootProps}
|
|
95
|
+
>
|
|
96
|
+
{/* Dropdown.Trigger를 통해 Select trigger를 그대로 재사용한다. */}
|
|
97
|
+
<Dropdown.Trigger asChild>
|
|
98
|
+
<SelectTriggerBase
|
|
99
|
+
ref={ref}
|
|
100
|
+
priority={priority}
|
|
101
|
+
size={size}
|
|
102
|
+
state={disabled ? "disabled" : state}
|
|
103
|
+
block={block}
|
|
104
|
+
open={dropdownOpen}
|
|
105
|
+
disabled={disabled}
|
|
106
|
+
buttonType={buttonType}
|
|
107
|
+
{...rest}
|
|
108
|
+
>
|
|
109
|
+
<SelectTriggerSelected
|
|
110
|
+
label={resolvedDisplayLabel}
|
|
111
|
+
placeholder={placeholder}
|
|
112
|
+
isPlaceholder={!hasLabel}
|
|
113
|
+
/>
|
|
114
|
+
</SelectTriggerBase>
|
|
115
|
+
</Dropdown.Trigger>
|
|
116
|
+
{shouldRenderDropdown ? (
|
|
117
|
+
<Dropdown.Container
|
|
118
|
+
{...dropdownContainerProps}
|
|
119
|
+
size={panelSize}
|
|
120
|
+
matchTriggerWidth={dropdownMatchTriggerWidth}
|
|
121
|
+
>
|
|
122
|
+
<Dropdown.Menu.List {...dropdownMenuListProps}>
|
|
123
|
+
{/* Dropdown menu option들을 그대로 매핑해 선택 이벤트를 전달한다. */}
|
|
124
|
+
{options.map(option => (
|
|
125
|
+
<Dropdown.Menu.Item
|
|
126
|
+
key={option.id}
|
|
127
|
+
label={option.label}
|
|
128
|
+
description={option.description}
|
|
129
|
+
disabled={option.disabled}
|
|
130
|
+
leftSlot={option.leftSlot}
|
|
131
|
+
rightSlot={option.rightSlot}
|
|
132
|
+
multiple={Boolean(option.multiple)}
|
|
133
|
+
isSelected={resolvedSelectedIds.includes(option.id)}
|
|
134
|
+
onSelect={event => {
|
|
135
|
+
if (option.disabled) {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
handleOptionSelect(option);
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</Dropdown.Menu.List>
|
|
144
|
+
</Dropdown.Container>
|
|
145
|
+
) : null}
|
|
146
|
+
</Dropdown.Root>
|
|
147
|
+
</Container>
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
SelectDefault.displayName = "SelectDefault";
|
|
153
|
+
|
|
154
|
+
export { SelectDefault };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
import { SlotComponent } from "../../../slot";
|
|
7
|
+
import { SelectIcon } from "./Icon";
|
|
8
|
+
import type { SelectTriggerBaseProps } from "../../types/trigger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Select trigger foundation; priority/size/state를 data attribute로 노출하고
|
|
12
|
+
* Chevron 아이콘을 자동 연결하는 기본 요소다.
|
|
13
|
+
* @component
|
|
14
|
+
* @param {SelectTriggerBaseProps} props trigger base props
|
|
15
|
+
* @param {"primary" | "secondary"} [props.priority="primary"] 스타일 우선순위
|
|
16
|
+
* @param {"small" | "medium" | "large"} [props.size="medium"] 높이 스케일
|
|
17
|
+
* @param {"default" | "focused" | "disabled"} [props.state="default"] 시각 상태
|
|
18
|
+
* @param {boolean} [props.multiple] multi select 여부
|
|
19
|
+
* @param {ElementType} [props.as="button"] polymorphic 태그
|
|
20
|
+
* @param {"button" | "submit" | "reset"} [props.buttonType="button"] native button type
|
|
21
|
+
*/
|
|
22
|
+
const SelectTriggerBase = forwardRef<HTMLElement, SelectTriggerBaseProps>(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
priority = "primary",
|
|
26
|
+
size = "medium",
|
|
27
|
+
state = "default",
|
|
28
|
+
open = false,
|
|
29
|
+
block = false,
|
|
30
|
+
multiple = false,
|
|
31
|
+
disabled = false,
|
|
32
|
+
as = "button",
|
|
33
|
+
buttonType = "button",
|
|
34
|
+
className,
|
|
35
|
+
children,
|
|
36
|
+
...rest
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) => {
|
|
40
|
+
const Icon = SelectIcon.Chevron[priority][size];
|
|
41
|
+
const {
|
|
42
|
+
["aria-haspopup"]: ariaHasPopup,
|
|
43
|
+
["aria-expanded"]: ariaExpanded,
|
|
44
|
+
...restProps
|
|
45
|
+
} = rest;
|
|
46
|
+
|
|
47
|
+
const sharedProps = {
|
|
48
|
+
className: clsx("select-button", className),
|
|
49
|
+
"data-priority": priority,
|
|
50
|
+
"data-size": size,
|
|
51
|
+
"data-state": state,
|
|
52
|
+
"data-open": open || undefined,
|
|
53
|
+
"data-multiple": multiple || undefined,
|
|
54
|
+
"data-block": block ? "true" : undefined,
|
|
55
|
+
"aria-haspopup": ariaHasPopup ?? "listbox",
|
|
56
|
+
"aria-expanded": ariaExpanded ?? open,
|
|
57
|
+
...restProps,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const elementSpecificProps =
|
|
61
|
+
typeof as === "string" && as === "button"
|
|
62
|
+
? {
|
|
63
|
+
type: buttonType,
|
|
64
|
+
disabled,
|
|
65
|
+
}
|
|
66
|
+
: {
|
|
67
|
+
role: "button",
|
|
68
|
+
tabIndex: disabled ? -1 : 0,
|
|
69
|
+
"aria-disabled": disabled ? "true" : undefined,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<SlotComponent
|
|
74
|
+
ref={ref}
|
|
75
|
+
as={as}
|
|
76
|
+
{...sharedProps}
|
|
77
|
+
{...elementSpecificProps}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
<figure className="select-icon" data-size={size} aria-hidden="true">
|
|
81
|
+
<Icon />
|
|
82
|
+
</figure>
|
|
83
|
+
</SlotComponent>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
SelectTriggerBase.displayName = "SelectBase";
|
|
89
|
+
|
|
90
|
+
export { SelectTriggerBase };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
|
|
5
|
+
import type { SelectContainerProps } from "../../types/props";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Select trigger/container wrapper
|
|
9
|
+
* @component
|
|
10
|
+
* @param {SelectContainerProps} props Select container props
|
|
11
|
+
* @param {string} [props.className] 사용자 정의 className
|
|
12
|
+
* @param {boolean} [props.block] wrapper 전체 폭 확장 여부
|
|
13
|
+
* @param {React.ReactNode} props.children trigger 및 dropdown 콘텐츠
|
|
14
|
+
*/
|
|
15
|
+
export default function SelectContainer({
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
block = false,
|
|
19
|
+
}: SelectContainerProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={clsx("select select-container", className, {
|
|
23
|
+
"select-block": block,
|
|
24
|
+
})}
|
|
25
|
+
>
|
|
26
|
+
{/** dropdown root 및 dropdown menu 등 포함 예정 */}
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|