@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.
Files changed (113) hide show
  1. package/README.md +2 -2
  2. package/dist/styles.css +1112 -385
  3. package/package.json +12 -15
  4. package/src/components/button/index.scss +1 -0
  5. package/src/components/button/markup/{ButtonRounded.tsx → Rounded.tsx} +1 -1
  6. package/src/components/button/markup/{ButtonText.tsx → Text.tsx} +1 -1
  7. package/src/components/button/markup/index.ts +3 -3
  8. package/src/components/button/styles/button.scss +113 -229
  9. package/src/components/button/styles/round-button.scss +11 -14
  10. package/src/components/button/styles/text-button.scss +23 -23
  11. package/src/components/button/styles/variables.scss +145 -0
  12. package/src/components/dropdown/index.tsx +3 -3
  13. package/src/components/dropdown/markup/Template.tsx +61 -0
  14. package/src/components/dropdown/markup/foundation/Container.tsx +97 -0
  15. package/src/components/dropdown/markup/foundation/MenuItem.tsx +107 -0
  16. package/src/components/dropdown/markup/foundation/MenuList.tsx +27 -0
  17. package/src/components/dropdown/markup/foundation/Provider.tsx +46 -0
  18. package/src/components/dropdown/markup/foundation/Root.tsx +30 -0
  19. package/src/components/dropdown/markup/foundation/Trigger.tsx +34 -0
  20. package/src/components/dropdown/markup/foundation/index.tsx +25 -0
  21. package/src/components/dropdown/markup/index.tsx +8 -2
  22. package/src/components/dropdown/styles/dropdown.scss +166 -0
  23. package/src/components/dropdown/styles/index.scss +2 -0
  24. package/src/components/dropdown/styles/variables.scss +40 -0
  25. package/src/components/dropdown/types/base.ts +18 -0
  26. package/src/components/dropdown/types/index.ts +2 -4
  27. package/src/components/dropdown/types/props.ts +174 -0
  28. package/src/components/dropdown/utils/index.ts +1 -4
  29. package/src/components/dropdown/utils/refs.ts +20 -0
  30. package/src/components/form/index.scss +1 -0
  31. package/src/components/form/index.tsx +18 -2
  32. package/src/components/form/markup/form-field/Body.tsx +18 -0
  33. package/src/components/form/markup/form-field/Container.tsx +58 -0
  34. package/src/components/form/markup/form-field/Footer.tsx +21 -0
  35. package/src/components/form/markup/form-field/Header.tsx +39 -0
  36. package/src/components/form/markup/form-field/Template.tsx +56 -0
  37. package/src/components/form/markup/form-field/index.tsx +22 -0
  38. package/src/components/form/styles/form-field/layout.scss +67 -0
  39. package/src/components/form/styles/form-field/variables.scss +17 -0
  40. package/src/components/form/styles/index.scss +2 -0
  41. package/src/components/form/types/index.ts +1 -0
  42. package/src/components/form/types/props.ts +125 -0
  43. package/src/components/form/utils/form-field.ts +42 -0
  44. package/src/components/input/hooks/index.ts +1 -4
  45. package/src/components/input/hooks/useDigitField.ts +63 -0
  46. package/src/components/input/img/calendar/calendar.svg +7 -0
  47. package/src/components/input/img/calendar/chevron-down.svg +3 -0
  48. package/src/components/input/img/calendar/chevron-left.svg +3 -0
  49. package/src/components/input/img/calendar/chevron-right.svg +3 -0
  50. package/src/components/input/img/calendar/chevron-up.svg +3 -0
  51. package/src/components/input/index.tsx +2 -1
  52. package/src/components/input/markup/calendar/Base.tsx +329 -0
  53. package/src/components/input/markup/calendar/index.tsx +8 -0
  54. package/src/components/input/markup/{text/InputUtilityButton.tsx → foundation/Button.tsx} +5 -15
  55. package/src/components/input/markup/foundation/Input.tsx +245 -0
  56. package/src/components/input/markup/foundation/SideSlot.tsx +30 -0
  57. package/src/components/input/markup/foundation/StatusIcon.tsx +21 -0
  58. package/src/components/input/markup/foundation/Utility.tsx +103 -0
  59. package/src/components/input/markup/foundation/index.tsx +15 -0
  60. package/src/components/input/markup/index.tsx +11 -1
  61. package/src/components/input/markup/text/AuthCode.tsx +41 -59
  62. package/src/components/input/markup/text/Email.tsx +25 -115
  63. package/src/components/input/markup/text/Password.tsx +30 -39
  64. package/src/components/input/markup/text/Phone.tsx +35 -122
  65. package/src/components/input/markup/text/Search.tsx +17 -18
  66. package/src/components/input/markup/text/index.ts +15 -12
  67. package/src/components/input/styles/calendar.scss +110 -0
  68. package/src/components/input/styles/foundation.scss +345 -0
  69. package/src/components/input/styles/index.scss +4 -476
  70. package/src/components/input/styles/text.scss +89 -0
  71. package/src/components/input/styles/variables.scss +41 -0
  72. package/src/components/input/types/calendar.ts +208 -0
  73. package/src/components/input/types/foundation.ts +194 -0
  74. package/src/components/input/types/hooks.ts +43 -0
  75. package/src/components/input/types/index.ts +5 -87
  76. package/src/components/input/types/text.ts +203 -0
  77. package/src/components/input/types/verification.ts +23 -0
  78. package/src/components/input/utils/index.tsx +1 -0
  79. package/src/components/input/utils/verification.tsx +35 -0
  80. package/src/components/select/hooks/index.ts +43 -2
  81. package/src/components/select/img/chevron/primary/large.svg +3 -0
  82. package/src/components/select/img/chevron/primary/medium.svg +3 -0
  83. package/src/components/select/img/chevron/primary/small.svg +3 -0
  84. package/src/components/select/img/chevron/secondary/large.svg +3 -0
  85. package/src/components/select/img/chevron/secondary/medium.svg +3 -0
  86. package/src/components/select/img/chevron/secondary/small.svg +3 -0
  87. package/src/components/select/img/remove.svg +3 -0
  88. package/src/components/select/index.scss +2 -1
  89. package/src/components/select/index.tsx +5 -0
  90. package/src/components/select/markup/Default.tsx +154 -0
  91. package/src/components/select/markup/foundation/Base.tsx +90 -0
  92. package/src/components/select/markup/foundation/Container.tsx +30 -0
  93. package/src/components/select/markup/foundation/Icon.tsx +78 -0
  94. package/src/components/select/markup/foundation/Selected.tsx +34 -0
  95. package/src/components/select/markup/foundation/index.ts +2 -0
  96. package/src/components/select/markup/index.tsx +36 -2
  97. package/src/components/select/markup/multiple/Multiple.tsx +205 -0
  98. package/src/components/select/markup/multiple/SelectedChip.tsx +58 -0
  99. package/src/components/select/markup/multiple/index.ts +2 -0
  100. package/src/components/select/styles/select.scss +316 -0
  101. package/src/components/select/styles/variables.scss +91 -0
  102. package/src/components/select/types/base.ts +34 -0
  103. package/src/components/select/types/icon.ts +45 -0
  104. package/src/components/select/types/index.ts +5 -4
  105. package/src/components/select/types/multiple.ts +57 -0
  106. package/src/components/select/types/props.ts +208 -0
  107. package/src/components/select/types/trigger.ts +196 -0
  108. package/src/index.scss +3 -2
  109. package/src/components/input/markup/text/Base.tsx +0 -454
  110. package/src/components/input/utils/index.ts +0 -60
  111. package/src/components/select/styles/index.scss +0 -0
  112. /package/src/components/button/markup/{ButtonDefault.tsx → Base.tsx} +0 -0
  113. /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
- * TODO(select): 접근성/상태 계산 hook을 정의한다.
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/index.scss";
1
+ @use "./styles/variables.scss";
2
+ @use "./styles/select.scss";
@@ -1,4 +1,9 @@
1
1
  /**
2
2
  * select 카테고리 배럴 placeholder: 실제 구현은 markup/ 하위에 추가한다.
3
3
  */
4
+ import "./index.scss";
5
+
4
6
  export * from "./markup";
7
+ export * from "./types";
8
+ export * from "./hooks";
9
+ export * from "./utils";
@@ -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
+ }