@uniai-fe/uds-primitives 0.2.1 → 0.2.2

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 (35) hide show
  1. package/dist/styles.css +99 -7
  2. package/package.json +5 -3
  3. package/src/components/button/index.tsx +0 -2
  4. package/src/components/button/markup/Base.tsx +22 -1
  5. package/src/components/button/styles/button.scss +24 -2
  6. package/src/components/button/styles/variables.scss +4 -0
  7. package/src/components/button/types/index.ts +7 -0
  8. package/src/components/checkbox/markup/Checkbox.tsx +31 -25
  9. package/src/components/dropdown/markup/index.tsx +10 -1
  10. package/src/components/input/hooks/index.ts +1 -0
  11. package/src/components/input/hooks/useAddress.ts +247 -0
  12. package/src/components/input/index.scss +5 -1
  13. package/src/components/input/markup/address/Button.tsx +65 -0
  14. package/src/components/input/markup/address/Template.tsx +135 -0
  15. package/src/components/input/markup/address/index.ts +9 -0
  16. package/src/components/input/markup/foundation/Input.tsx +20 -1
  17. package/src/components/input/markup/index.tsx +2 -0
  18. package/src/components/input/styles/address.scss +24 -0
  19. package/src/components/input/styles/foundation.scss +28 -2
  20. package/src/components/input/styles/variables.scss +4 -0
  21. package/src/components/input/types/address.ts +249 -0
  22. package/src/components/input/types/foundation.ts +6 -0
  23. package/src/components/input/types/index.ts +1 -0
  24. package/src/components/input/utils/address.ts +165 -0
  25. package/src/components/input/utils/index.tsx +1 -0
  26. package/src/components/radio/markup/Radio.tsx +10 -2
  27. package/src/components/radio/markup/RadioCard.tsx +6 -1
  28. package/src/components/radio/markup/RadioCardGroup.tsx +6 -1
  29. package/src/components/select/markup/Default.tsx +2 -0
  30. package/src/components/select/markup/foundation/Container.tsx +23 -0
  31. package/src/components/select/markup/multiple/Multiple.tsx +2 -0
  32. package/src/components/select/styles/select.scss +25 -2
  33. package/src/components/select/styles/variables.scss +4 -0
  34. package/src/components/select/types/props.ts +24 -5
  35. package/src/components/input/styles/index.scss +0 -4
@@ -0,0 +1,165 @@
1
+ import { string as toString } from "@uniai-fe/util-functions";
2
+ import type { Address } from "react-daum-postcode";
3
+
4
+ import type {
5
+ AddressResolvedValue,
6
+ AddressSearchResult,
7
+ AddressStructure,
8
+ } from "../types/address";
9
+
10
+ /**
11
+ * 객체 여부 체크 util.
12
+ */
13
+ const isObject = (value: unknown): value is Record<string, unknown> =>
14
+ typeof value === "object" && value !== null;
15
+
16
+ /**
17
+ * react-daum-postcode AddressSearchResult 유사 객체인지 검사한다.
18
+ */
19
+ const isAddressSearchResultLike = (
20
+ value: unknown,
21
+ ): value is Partial<AddressSearchResult> =>
22
+ isObject(value) && typeof value.address === "string";
23
+
24
+ /**
25
+ * AddressStructure 형태인지 검사한다.
26
+ */
27
+ const isAddressStructureLike = (
28
+ value: unknown,
29
+ ): value is Partial<AddressStructure> =>
30
+ isObject(value) &&
31
+ ("main" in value || "extras" in value) &&
32
+ (typeof value.main === "string" || Array.isArray(value.extras));
33
+
34
+ /**
35
+ * 문자열 파트 배열을 안전하게 변환한다.
36
+ */
37
+ const toAddressParts = (items: unknown[] | undefined): string[] => {
38
+ if (!Array.isArray(items)) return [];
39
+ return items
40
+ .map(item => toString(item, ""))
41
+ .filter((segment): segment is string => segment.length > 0);
42
+ };
43
+
44
+ /**
45
+ * AddressStructure에서 main/extras를 순서대로 병합한다.
46
+ */
47
+ const mergeStructureParts = (
48
+ structure: AddressStructure | Partial<AddressStructure>,
49
+ ): string[] => {
50
+ const mainPart =
51
+ typeof structure.main === "string" ? structure.main : undefined;
52
+ const extras = toAddressParts(structure.extras);
53
+ return [mainPart, ...extras].filter(
54
+ (part): part is string => typeof part === "string" && part.length > 0,
55
+ );
56
+ };
57
+
58
+ /**
59
+ * Template/Base 컴포넌트에서 사용하는 주소 문자열 정규화.
60
+ * @function
61
+ * @param {unknown} value RHF watch 값 혹은 addressInput.value override
62
+ * @returns {AddressResolvedValue} 문자열과 파트 배열
63
+ */
64
+ export const resolveAddressValue = (value: unknown): AddressResolvedValue => {
65
+ if (typeof value === "string") {
66
+ return {
67
+ text: value,
68
+ parts: value.length > 0 ? [value] : [],
69
+ };
70
+ }
71
+
72
+ if (Array.isArray(value)) {
73
+ const parts = toAddressParts(value);
74
+ return {
75
+ text: parts.join(" "),
76
+ parts,
77
+ };
78
+ }
79
+
80
+ if (isAddressSearchResultLike(value)) {
81
+ const base = value.address ?? "";
82
+ const parts =
83
+ value.addressParts && value.addressParts.length > 0
84
+ ? toAddressParts(value.addressParts)
85
+ : value.addressStructure
86
+ ? mergeStructureParts(value.addressStructure)
87
+ : base
88
+ ? [base]
89
+ : [];
90
+ return {
91
+ text: base,
92
+ parts,
93
+ };
94
+ }
95
+
96
+ if (isAddressStructureLike(value)) {
97
+ const parts = mergeStructureParts(value);
98
+ return {
99
+ text: parts.join(" "),
100
+ parts,
101
+ };
102
+ }
103
+
104
+ if (isObject(value) && "text" in value) {
105
+ const textValue =
106
+ typeof (value as { text?: unknown }).text === "string"
107
+ ? (value as { text: string }).text
108
+ : "";
109
+ const parts = Array.isArray((value as { parts?: unknown[] }).parts)
110
+ ? toAddressParts((value as { parts: unknown[] }).parts)
111
+ : textValue
112
+ ? [textValue]
113
+ : [];
114
+ return {
115
+ text: textValue,
116
+ parts,
117
+ };
118
+ }
119
+
120
+ return { text: "", parts: [] };
121
+ };
122
+
123
+ /**
124
+ * react-daum-postcode Address 데이터를 AddressSearchResult 형식으로 변환한다.
125
+ * `address` 필드는 우편번호 서비스에서 내려준 최종 문자열(도로명 + 추가 항목)을 그대로 유지하고,
126
+ * 분해 정보(`addressParts`, `addressStructure`)를 추가로 제공한다.
127
+ * @function
128
+ * @see https://www.juso.go.kr/addrlink/devAddrLinkRequestGuide.do
129
+ * @param {Address} data 우편번호 서비스 선택 결과
130
+ * @returns {AddressSearchResult} 변환된 주소 정보 객체
131
+ * @example
132
+ * ```ts
133
+ * const result = createAddressSearchResult(data);
134
+ * console.log(result.address); // "서울시 강남구 ... (OO동)"
135
+ * console.log(result.addressParts); // ["서울시 강남구 ...", "OO동"]
136
+ * ```
137
+ */
138
+ export const createAddressSearchResult = (
139
+ data: Address,
140
+ ): AddressSearchResult => {
141
+ const extraParts: string[] = [];
142
+
143
+ if (data.addressType === "R") {
144
+ if (data.bname) extraParts.push(data.bname);
145
+ if (data.buildingName) extraParts.push(data.buildingName);
146
+ }
147
+
148
+ const baseAddress = data.address;
149
+ const labelExtras =
150
+ extraParts.length > 0 ? ` (${extraParts.join(", ")})` : "";
151
+ const parts = [baseAddress, ...extraParts].filter(part => part.length > 0);
152
+
153
+ return {
154
+ address: `${baseAddress}${labelExtras}`,
155
+ addressParts: parts,
156
+ addressStructure: {
157
+ main: baseAddress,
158
+ extras: [...extraParts],
159
+ },
160
+ zipCode: data.zonecode,
161
+ buildingName: data.buildingName || undefined,
162
+ district: data.bname || undefined,
163
+ raw: data,
164
+ };
165
+ };
@@ -1 +1,2 @@
1
1
  export * from "./verification";
2
+ export * from "./address";
@@ -26,7 +26,7 @@ const RADIO_HELPER_CLASSNAME = "radio-helper";
26
26
  * @example
27
27
  * <Radio value="option" />
28
28
  */
29
- export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
29
+ const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
30
30
  { size = "medium", className, disabled, ...restProps },
31
31
  ref,
32
32
  ) {
@@ -49,6 +49,9 @@ export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
49
49
  );
50
50
  });
51
51
 
52
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
53
+ Radio.displayName = "Radio";
54
+
52
55
  /**
53
56
  * RadioField component; label/helper wrapper for single radioItem
54
57
  * @component
@@ -66,7 +69,7 @@ export const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(
66
69
  * @example
67
70
  * <RadioField label="옵션" helperText="필수" value="option" />
68
71
  */
69
- export const RadioField = forwardRef<HTMLButtonElement, RadioFieldProps>(
72
+ const RadioField = forwardRef<HTMLButtonElement, RadioFieldProps>(
70
73
  function RadioField(
71
74
  {
72
75
  label,
@@ -133,3 +136,8 @@ export const RadioField = forwardRef<HTMLButtonElement, RadioFieldProps>(
133
136
  );
134
137
  },
135
138
  );
139
+
140
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
141
+ RadioField.displayName = "RadioField";
142
+
143
+ export { Radio, RadioField };
@@ -30,7 +30,7 @@ const RADIO_CARD_INDICATOR_CLASSNAME = "radio-card-indicator";
30
30
  * @example
31
31
  * <RadioCard title="행복1농장" value="farm-1" />
32
32
  */
33
- export const RadioCard = forwardRef<HTMLButtonElement, RadioCardProps>(
33
+ const RadioCard = forwardRef<HTMLButtonElement, RadioCardProps>(
34
34
  function RadioCard(
35
35
  {
36
36
  title,
@@ -74,3 +74,8 @@ export const RadioCard = forwardRef<HTMLButtonElement, RadioCardProps>(
74
74
  );
75
75
  },
76
76
  );
77
+
78
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
79
+ RadioCard.displayName = "RadioCard";
80
+
81
+ export { RadioCard };
@@ -22,7 +22,7 @@ const RADIO_CARD_GROUP_CLASSNAME = "radio-card-group";
22
22
  * @example
23
23
  * <RadioCardGroup options={[{ id: "farm-1", title: "농장" }]} value="farm-1" />
24
24
  */
25
- export const RadioCardGroup = forwardRef<
25
+ const RadioCardGroup = forwardRef<
26
26
  ElementRef<typeof RadixRadioGroup.Root>,
27
27
  RadioCardGroupProps
28
28
  >(function RadioCardGroup(
@@ -81,3 +81,8 @@ export const RadioCardGroup = forwardRef<
81
81
  </RadixRadioGroup.Root>
82
82
  );
83
83
  });
84
+
85
+ // forwardRef Tooltip 유지를 위해 displayName 지정.
86
+ RadioCardGroup.displayName = "RadioCardGroup";
87
+
88
+ export { RadioCardGroup };
@@ -36,6 +36,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
36
36
  size = "medium",
37
37
  state = "default",
38
38
  block,
39
+ width,
39
40
  isOpen,
40
41
  disabled,
41
42
  buttonType,
@@ -86,6 +87,7 @@ const SelectDefault = forwardRef<HTMLElement, SelectDefaultComponentProps>(
86
87
  <Container
87
88
  className={clsx("select-trigger-container", className)}
88
89
  block={block}
90
+ width={width}
89
91
  >
90
92
  <Dropdown.Root
91
93
  open={dropdownOpen}
@@ -3,6 +3,10 @@
3
3
  import clsx from "clsx";
4
4
 
5
5
  import type { SelectContainerProps } from "../../types/props";
6
+ import {
7
+ getFormFieldWidthAttr,
8
+ getFormFieldWidthValue,
9
+ } from "../../../form/utils/form-field";
6
10
 
7
11
  /**
8
12
  * Select trigger/container wrapper
@@ -16,12 +20,31 @@ export default function SelectContainer({
16
20
  className,
17
21
  children,
18
22
  block = false,
23
+ width,
24
+ style,
25
+ ...restProps
19
26
  }: SelectContainerProps) {
27
+ const widthAttr =
28
+ width !== undefined
29
+ ? getFormFieldWidthAttr(width)
30
+ : block
31
+ ? "full"
32
+ : undefined;
33
+ const widthValue =
34
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
35
+ const mergedStyle =
36
+ widthValue !== undefined
37
+ ? { ...(style ?? {}), ["--select-width" as const]: widthValue }
38
+ : style;
39
+
20
40
  return (
21
41
  <div
22
42
  className={clsx("select select-container", className, {
23
43
  "select-block": block,
24
44
  })}
45
+ data-width={widthAttr}
46
+ style={mergedStyle}
47
+ {...restProps}
25
48
  >
26
49
  {/** dropdown root 및 dropdown menu 등 포함 예정 */}
27
50
  {children}
@@ -38,6 +38,7 @@ const SelectMultipleTrigger = forwardRef<
38
38
  size = "medium",
39
39
  state = "default",
40
40
  block,
41
+ width,
41
42
  isOpen,
42
43
  disabled,
43
44
  tags,
@@ -113,6 +114,7 @@ const SelectMultipleTrigger = forwardRef<
113
114
  <Container
114
115
  className={clsx("select-trigger-multiple", className)}
115
116
  block={block}
117
+ width={width}
116
118
  >
117
119
  <Dropdown.Root
118
120
  open={dropdownOpen}
@@ -1,12 +1,35 @@
1
1
  .select {
2
2
  display: flex;
3
- width: 100%;
3
+ width: var(--select-width);
4
+ flex: var(--select-flex);
4
5
  flex-direction: column;
5
6
  gap: var(--spacing-gap-2);
7
+ min-width: 0;
8
+ }
9
+
10
+ .select[data-width="auto"] {
11
+ --select-width: auto;
12
+ --select-flex: 0 1 auto;
13
+ }
14
+
15
+ .select[data-width="fill"] {
16
+ --select-width: auto;
17
+ --select-flex: 1 1 0%;
6
18
  }
7
19
 
20
+ .select[data-width="full"],
8
21
  .select-block {
9
- width: 100%;
22
+ --select-width: 100%;
23
+ --select-flex: 0 0 100%;
24
+ }
25
+
26
+ .select[data-width="fit"] {
27
+ --select-width: fit-content;
28
+ --select-flex: 0 0 auto;
29
+ }
30
+
31
+ .select[data-width="custom"] {
32
+ --select-flex: 0 0 auto;
10
33
  }
11
34
 
12
35
  .select-button {
@@ -1,5 +1,9 @@
1
1
  /* Select tokens mapped to Input primary tokens for visual parity */
2
2
  :root {
3
+ /* layout presets */
4
+ --select-width: 100%;
5
+ --select-flex: 0 1 auto;
6
+
3
7
  --select-primary-height-small: var(--input-default-height-small);
4
8
  --select-primary-height-medium: var(--input-default-height-medium);
5
9
  --select-primary-height-large: var(--input-default-height-large);
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
2
 
3
3
  import type {
4
4
  DropdownContainerProps,
@@ -7,6 +7,7 @@ import type {
7
7
  DropdownRootProps,
8
8
  DropdownTemplateItem,
9
9
  } from "../../dropdown/types";
10
+ import type { FormFieldWidth } from "../../form/types";
10
11
  import type { SelectPriority, SelectSize, SelectState } from "./base";
11
12
  import type {
12
13
  SelectTriggerDefaultProps,
@@ -82,7 +83,7 @@ export interface SelectComponentState {
82
83
  * @property {ReactNode} children trigger/Dropdown 등 내부 콘텐츠
83
84
  * @property {boolean} [block] block 여부
84
85
  */
85
- export interface SelectContainerProps {
86
+ export interface SelectContainerProps extends HTMLAttributes<HTMLDivElement> {
86
87
  /**
87
88
  * 사용자 정의 className
88
89
  */
@@ -95,6 +96,21 @@ export interface SelectContainerProps {
95
96
  * block 여부
96
97
  */
97
98
  block?: boolean;
99
+ /**
100
+ * width preset
101
+ */
102
+ width?: FormFieldWidth;
103
+ }
104
+
105
+ /**
106
+ * Select width 옵션
107
+ * @property {FormFieldWidth} [width] width preset 옵션
108
+ */
109
+ export interface SelectWidthOption {
110
+ /**
111
+ * width preset 옵션
112
+ */
113
+ width?: FormFieldWidth;
98
114
  }
99
115
 
100
116
  /**
@@ -103,7 +119,8 @@ export interface SelectContainerProps {
103
119
  */
104
120
  export type SelectProps = SelectStyleOptions &
105
121
  SelectValueOptions &
106
- SelectComponentState;
122
+ SelectComponentState &
123
+ SelectWidthOption;
107
124
 
108
125
  /**
109
126
  * Select dropdown option; Dropdown.Template item 계약을 그대로 따른다.
@@ -194,7 +211,8 @@ export interface SelectDefaultComponentProps
194
211
  extends
195
212
  SelectTriggerDefaultProps,
196
213
  SelectDropdownConfigProps,
197
- SelectDropdownBehaviorProps {}
214
+ SelectDropdownBehaviorProps,
215
+ SelectWidthOption {}
198
216
 
199
217
  /**
200
218
  * Select.Multiple 컴포넌트 props
@@ -206,4 +224,5 @@ export interface SelectMultipleComponentProps
206
224
  extends
207
225
  SelectTriggerMultipleProps,
208
226
  SelectDropdownConfigProps,
209
- SelectDropdownBehaviorProps {}
227
+ SelectDropdownBehaviorProps,
228
+ SelectWidthOption {}
@@ -1,4 +0,0 @@
1
- @use "./variables.scss" as inputVariables;
2
- @use "./foundation.scss" as inputFoundation;
3
- @use "./text.scss" as inputText;
4
- @use "./calendar.scss" as inputCalendar;