@uniai-fe/uds-primitives 0.3.21 → 0.3.23

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 CHANGED
@@ -114,6 +114,13 @@ function Templates() {
114
114
 
115
115
  - TextInput Base 컴포넌트의 `clear` 버튼 로직을 pointer 이벤트 기반으로 재작성해 모바일 터치 환경에서도 안정적으로 입력값이 초기화되도록 했다.
116
116
  - react-hook-form `register`와 연계된 값도 동일하게 초기화되며, focus가 빠지면 clear 버튼이 자동으로 숨겨진다.
117
+ - `Input.TextArea`를 추가했다.
118
+ - `height?: number | string`으로 높이를 직접 제어한다. (기본 `128px`)
119
+ - `length?: number`을 주면 `0 / n자` 카운터가 우측 하단에 노출된다.
120
+ - 기본 `resize`는 비활성(`none`)이며, `size` 축은 Input 기본 size token(`--input-default-*`)을 재사용한다.
121
+ - Select/Dropdown 계약을 `items` 중심으로 정렬했다.
122
+ - Select: `items`, `onSelectChange`, `dropdownOptions`, `open/defaultOpen/onOpen`
123
+ - Dropdown.Template: `items[].selected + onChange(payload)`
117
124
 
118
125
  ## 스타일 내보내기
119
126
 
package/dist/styles.css CHANGED
@@ -361,6 +361,27 @@
361
361
  --input-tertiary-radius-base: var(--theme-radius-large-2);
362
362
  --input-table-radius-base: 0;
363
363
  --input-tertiary-element-min-height: var(--theme-size-medium-2);
364
+ --input-textarea-min-height: calc(var(--input-default-height-medium) * 2);
365
+ --input-textarea-height: 128px;
366
+ /* TextArea size token은 Input 기본 size token을 재할당해 축을 일치시킨다. */
367
+ --input-textarea-radius-small: var(--input-default-radius-base);
368
+ --input-textarea-radius-medium: var(--input-default-radius-base);
369
+ --input-textarea-radius-large: var(--input-default-radius-base);
370
+ --input-textarea-padding-inline-small: var(--spacing-padding-6);
371
+ --input-textarea-padding-inline-medium: var(--spacing-padding-6);
372
+ --input-textarea-padding-inline-large: var(--spacing-padding-6);
373
+ --input-textarea-padding-block-small: var(--spacing-padding-5);
374
+ --input-textarea-padding-block-medium: var(--spacing-padding-5);
375
+ --input-textarea-padding-block-large: var(--spacing-padding-6);
376
+ --input-textarea-gap-small: var(--input-default-gap);
377
+ --input-textarea-gap-medium: var(--input-default-gap);
378
+ --input-textarea-gap-large: var(--input-default-gap);
379
+ --input-textarea-counter-font-size: var(--font-label-medium-size);
380
+ --input-textarea-counter-line-height: var(--font-label-medium-line-height);
381
+ --input-textarea-counter-font-weight: var(--font-label-medium-weight);
382
+ --input-textarea-counter-letter-spacing: var(
383
+ --font-label-medium-letter-spacing
384
+ );
364
385
  --input-table-text-small-size: var(--font-body-xxsmall-size);
365
386
  --input-table-text-small-line-height: var(--font-body-xxsmall-line-height);
366
387
  --input-table-text-small-weight: var(--font-body-xxsmall-weight);
@@ -2288,6 +2309,9 @@ figure.chip {
2288
2309
  font-weight: var(--dropdown-option-font-weight, var(--dropdown-text-weight));
2289
2310
  line-height: var(--dropdown-option-line-height, var(--dropdown-text-medium-line-height));
2290
2311
  }
2312
+ .dropdown-menu-item-trigger span {
2313
+ user-select: none;
2314
+ }
2291
2315
  .dropdown-menu-item-trigger[data-state=selected] {
2292
2316
  background-color: var(--dropdown-option-bg-selected);
2293
2317
  color: var(--dropdown-option-color-selected);
@@ -2314,7 +2338,7 @@ figure.chip {
2314
2338
 
2315
2339
  .dropdown-menu-item-left,
2316
2340
  .dropdown-menu-item-right {
2317
- display: inline-flex;
2341
+ display: flex;
2318
2342
  align-items: center;
2319
2343
  color: inherit;
2320
2344
  }
@@ -2328,7 +2352,7 @@ figure.chip {
2328
2352
  }
2329
2353
 
2330
2354
  .dropdown-menu-item-label {
2331
- display: inline-flex;
2355
+ display: flex;
2332
2356
  align-items: center;
2333
2357
  min-width: 0;
2334
2358
  color: inherit;
@@ -2730,6 +2754,47 @@ figure.chip {
2730
2754
  min-width: 0;
2731
2755
  }
2732
2756
 
2757
+ .input[data-input-type=textarea] .input-field {
2758
+ --input-textarea-radius: var(--input-textarea-radius-medium);
2759
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-medium);
2760
+ --input-textarea-padding-block: var(--input-textarea-padding-block-medium);
2761
+ --input-textarea-gap: var(--input-textarea-gap-medium);
2762
+ align-items: stretch;
2763
+ flex-direction: column;
2764
+ gap: var(--input-textarea-gap);
2765
+ border-radius: var(--input-textarea-radius);
2766
+ padding: var(--input-textarea-padding-block) var(--input-textarea-padding-inline);
2767
+ }
2768
+ .input[data-input-type=textarea] .input-field[data-size=small] {
2769
+ --input-textarea-radius: var(--input-textarea-radius-small);
2770
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-small);
2771
+ --input-textarea-padding-block: var(--input-textarea-padding-block-small);
2772
+ --input-textarea-gap: var(--input-textarea-gap-small);
2773
+ }
2774
+ .input[data-input-type=textarea] .input-field[data-size=large] {
2775
+ --input-textarea-radius: var(--input-textarea-radius-large);
2776
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-large);
2777
+ --input-textarea-padding-block: var(--input-textarea-padding-block-large);
2778
+ --input-textarea-gap: var(--input-textarea-gap-large);
2779
+ }
2780
+
2781
+ .input-textarea-element {
2782
+ min-height: var(--input-textarea-min-height);
2783
+ height: var(--input-textarea-height);
2784
+ resize: none;
2785
+ margin: 0;
2786
+ }
2787
+
2788
+ .input-textarea-length {
2789
+ margin: 0;
2790
+ align-self: flex-end;
2791
+ color: var(--input-placeholder-color);
2792
+ font-size: var(--input-textarea-counter-font-size);
2793
+ line-height: var(--input-textarea-counter-line-height);
2794
+ font-weight: var(--input-textarea-counter-font-weight);
2795
+ letter-spacing: var(--input-textarea-counter-letter-spacing);
2796
+ }
2797
+
2733
2798
  .input-field-utilities {
2734
2799
  display: flex;
2735
2800
  align-items: center;
@@ -2851,11 +2916,15 @@ figure.chip {
2851
2916
  .input[data-state=error] .input-helper-text {
2852
2917
  color: var(--input-label-error-color);
2853
2918
  }
2919
+ .input[data-state=error] .input-textarea-length {
2920
+ color: var(--input-label-error-color);
2921
+ }
2854
2922
 
2855
2923
  .input[data-state=disabled] .input-label,
2856
2924
  .input[data-state=disabled] .input-inline-label,
2857
2925
  .input[data-state=disabled] .input-helper-text,
2858
- .input[data-state=disabled] .input-affix {
2926
+ .input[data-state=disabled] .input-affix,
2927
+ .input[data-state=disabled] .input-textarea-length {
2859
2928
  color: var(--input-helper-disabled-color);
2860
2929
  }
2861
2930
  .input[data-state=disabled] .input-field {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -7,6 +7,7 @@ import { forwardRef } from "react";
7
7
  import type { DropdownMenuItemProps } from "../../types/props";
8
8
  import { Checkbox } from "../../../checkbox/markup/Checkbox";
9
9
  import type { CheckboxProps } from "../../../checkbox/types";
10
+ import { Slot } from "../../../slot";
10
11
 
11
12
  /**
12
13
  * Dropdown Foundation; Menu Item 옵션 렌더링 컴포넌트
@@ -42,13 +43,10 @@ const DropdownMenuItem = forwardRef<HTMLDivElement, DropdownMenuItemProps>(
42
43
  ref,
43
44
  ) => {
44
45
  const labelContent = label ?? children;
45
- // 변경: label/children이 string|number일 때만 준비된 label span으로 매핑하고, ReactNode는 그대로 렌더링한다.
46
- const resolvedLabelContent =
47
- typeof labelContent === "string" || typeof labelContent === "number" ? (
48
- <span className="dropdown-menu-item-label">{labelContent}</span>
49
- ) : (
50
- labelContent
51
- );
46
+ // 변경: label 렌더링을 Slot.Text 경로로 통일해 string|number만 공통 래핑 규칙을 적용한다.
47
+ const resolvedLabelContent = (
48
+ <Slot.Text className="dropdown-menu-item-label">{labelContent}</Slot.Text>
49
+ );
52
50
  const hasDescription = Boolean(description);
53
51
  const shouldRenderCheckbox = multiple && !left;
54
52
 
@@ -98,6 +98,10 @@
98
98
  var(--dropdown-text-medium-line-height)
99
99
  );
100
100
 
101
+ span {
102
+ user-select: none;
103
+ }
104
+
101
105
  &[data-state="selected"] {
102
106
  background-color: var(--dropdown-option-bg-selected);
103
107
  color: var(--dropdown-option-color-selected);
@@ -134,7 +138,8 @@
134
138
 
135
139
  .dropdown-menu-item-left,
136
140
  .dropdown-menu-item-right {
137
- display: inline-flex;
141
+ // 변경: inline-* 금지 규칙에 맞춰 정렬 컨테이너를 flex로 통일한다.
142
+ display: flex;
138
143
  align-items: center;
139
144
  color: inherit;
140
145
  }
@@ -148,7 +153,8 @@
148
153
  }
149
154
 
150
155
  .dropdown-menu-item-label {
151
- display: inline-flex;
156
+ // 변경: inline-* 금지 규칙에 맞춰 라벨 렌더 박스를 flex로 유지한다.
157
+ display: flex;
152
158
  align-items: center;
153
159
  min-width: 0;
154
160
  color: inherit;
@@ -0,0 +1,236 @@
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import type { CSSProperties, ChangeEvent, FocusEvent } from "react";
5
+ import {
6
+ forwardRef,
7
+ useCallback,
8
+ useEffect,
9
+ useId,
10
+ useMemo,
11
+ useState,
12
+ } from "react";
13
+ import type { InputTextAreaProps } from "../../types";
14
+ import {
15
+ getFormFieldWidthAttr,
16
+ getFormFieldWidthValue,
17
+ } from "../../../form/utils/form-field";
18
+
19
+ /**
20
+ * Native `<textarea>` 기반 텍스트 영역.
21
+ * Input.Base와 동일한 priority/size/state/width/register 계약을 사용한다.
22
+ *
23
+ * @component
24
+ * @param {InputTextAreaProps} props TextArea 컴포넌트 공통 props
25
+ */
26
+ const InputTextArea = forwardRef<HTMLTextAreaElement, InputTextAreaProps>(
27
+ (
28
+ {
29
+ priority = "primary",
30
+ size = "medium",
31
+ state: stateProp = "default",
32
+ block = false,
33
+ width,
34
+ inlineLabel,
35
+ inputClassName,
36
+ boxClassName: boxClassNameProp,
37
+ disabled,
38
+ id,
39
+ className,
40
+ register,
41
+ "data-simulated-state": simulatedState,
42
+ height,
43
+ length,
44
+ maxLength,
45
+ value,
46
+ defaultValue,
47
+ name,
48
+ onChange,
49
+ onFocus,
50
+ onBlur,
51
+ ...restProps
52
+ },
53
+ ref,
54
+ ) => {
55
+ const isReadOnly = restProps.readOnly === true;
56
+ // table priority는 width 지정이 없으면 셀 가로폭을 기본으로 채운다.
57
+ const isTablePriority = priority === "table";
58
+ const resolvedBlock = block || (isTablePriority && width === undefined);
59
+ // tertiary는 디자인 계약상 small 단일 사이즈만 사용한다.
60
+ const resolvedSize = priority === "tertiary" ? "small" : size;
61
+ const generatedId = useId();
62
+ const registerRef = register?.ref;
63
+ const registerOnChange = register?.onChange;
64
+ const registerOnBlur = register?.onBlur;
65
+ const registerName = register?.name;
66
+ const [isFocused, setIsFocused] = useState(false);
67
+ const [currentLength, setCurrentLength] = useState(() => {
68
+ const initial = value ?? defaultValue;
69
+ return initial !== undefined && initial !== null
70
+ ? String(initial).length
71
+ : 0;
72
+ });
73
+
74
+ useEffect(() => {
75
+ if (disabled || stateProp === "disabled" || isReadOnly) {
76
+ setIsFocused(false);
77
+ }
78
+ }, [disabled, isReadOnly, stateProp]);
79
+
80
+ useEffect(() => {
81
+ if (value !== undefined && value !== null) {
82
+ setCurrentLength(String(value).length);
83
+ }
84
+ }, [value]);
85
+
86
+ const currentState = disabled ? "disabled" : stateProp;
87
+ const isDisabled =
88
+ currentState === "disabled" || currentState === "loading";
89
+ // tertiary는 status feedback(success/error/active/focused) 컬러를 적용하지 않는다.
90
+ const visualState =
91
+ priority === "tertiary"
92
+ ? isDisabled
93
+ ? "disabled"
94
+ : "default"
95
+ : !isDisabled && isFocused
96
+ ? "active"
97
+ : currentState;
98
+
99
+ const setTextAreaRef = useCallback(
100
+ (node: HTMLTextAreaElement | null) => {
101
+ if (typeof ref === "function") {
102
+ ref(node);
103
+ } else if (ref) {
104
+ ref.current = node;
105
+ }
106
+ registerRef?.(node);
107
+ },
108
+ [ref, registerRef],
109
+ );
110
+
111
+ const handleFocus = (event: FocusEvent<HTMLTextAreaElement>) => {
112
+ if (!isReadOnly) {
113
+ setIsFocused(true);
114
+ }
115
+ onFocus?.(event);
116
+ };
117
+
118
+ const handleBlur = (event: FocusEvent<HTMLTextAreaElement>) => {
119
+ setIsFocused(false);
120
+ registerOnBlur?.(event);
121
+ onBlur?.(event);
122
+ };
123
+
124
+ const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
125
+ setCurrentLength(event.currentTarget.value.length);
126
+ registerOnChange?.(event);
127
+ onChange?.(event);
128
+ };
129
+
130
+ const textAreaName = registerName ?? name;
131
+ const widthAttr =
132
+ width !== undefined
133
+ ? getFormFieldWidthAttr(width)
134
+ : resolvedBlock
135
+ ? "full"
136
+ : undefined;
137
+ const widthValue =
138
+ width !== undefined ? getFormFieldWidthValue(width) : undefined;
139
+ // height는 runtime 값이므로 CSS variable을 인라인 주입한다.
140
+ const resolvedHeight = useMemo(() => {
141
+ if (typeof height === "number") {
142
+ return `${height}px`;
143
+ }
144
+ if (typeof height === "string" && height.length > 0) {
145
+ return height;
146
+ }
147
+ return "128px";
148
+ }, [height]);
149
+ const containerStyle = {
150
+ ...(widthValue !== undefined
151
+ ? ({ ["--input-width" as const]: widthValue } as CSSProperties)
152
+ : {}),
153
+ ["--input-textarea-height" as const]: resolvedHeight,
154
+ } as CSSProperties;
155
+ const showLength = typeof length === "number";
156
+ const resolvedMaxLength = maxLength ?? length;
157
+
158
+ return (
159
+ <div
160
+ className={clsx(
161
+ "input",
162
+ "input-type-textarea",
163
+ `input-priority-${priority}`,
164
+ `input-size-${resolvedSize}`,
165
+ `input-state-${visualState}`,
166
+ resolvedBlock && "input-block",
167
+ className,
168
+ )}
169
+ data-input-type="textarea"
170
+ data-priority={priority}
171
+ data-size={resolvedSize}
172
+ data-state={visualState}
173
+ data-readonly={isReadOnly ? "true" : undefined}
174
+ data-block={resolvedBlock ? "true" : undefined}
175
+ {...(simulatedState ? { "data-simulated-state": simulatedState } : {})}
176
+ data-width={widthAttr}
177
+ style={containerStyle}
178
+ >
179
+ <div
180
+ className={clsx(
181
+ "input-box",
182
+ `input-box-priority-${priority}`,
183
+ `input-box-size-${resolvedSize}`,
184
+ `input-box-state-${visualState}`,
185
+ resolvedBlock && "input-box-block",
186
+ boxClassNameProp,
187
+ )}
188
+ data-slot="box"
189
+ >
190
+ <div
191
+ className="input-field"
192
+ data-state={visualState}
193
+ data-priority={priority}
194
+ data-size={resolvedSize}
195
+ data-readonly={isReadOnly ? "true" : undefined}
196
+ data-block={resolvedBlock ? "true" : undefined}
197
+ >
198
+ <div className="input-field-control">
199
+ {priority === "tertiary" && inlineLabel ? (
200
+ // tertiary 라벨은 FormField header가 아니라 input border 내부에 노출한다.
201
+ <span className="input-inline-label">{inlineLabel}</span>
202
+ ) : null}
203
+ <textarea
204
+ {...restProps}
205
+ id={id ?? generatedId}
206
+ ref={setTextAreaRef}
207
+ className={clsx(
208
+ "input-element",
209
+ "input-textarea-element",
210
+ inputClassName,
211
+ )}
212
+ disabled={isDisabled}
213
+ aria-invalid={currentState === "error" ? true : undefined}
214
+ name={textAreaName}
215
+ maxLength={resolvedMaxLength}
216
+ value={value}
217
+ defaultValue={defaultValue}
218
+ onChange={handleChange}
219
+ onFocus={handleFocus}
220
+ onBlur={handleBlur}
221
+ />
222
+ </div>
223
+ {showLength ? (
224
+ // length prop이 있을 때만 카운터를 노출한다.
225
+ <p className="input-textarea-length">{`${currentLength} / ${length}자`}</p>
226
+ ) : null}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ );
231
+ },
232
+ );
233
+
234
+ InputTextArea.displayName = "TextArea";
235
+
236
+ export default InputTextArea;
@@ -1,4 +1,5 @@
1
1
  import InputBase from "./Input";
2
+ import InputTextArea from "./TextArea";
2
3
  import InputBaseSideSlot from "./SideSlot";
3
4
  import InputBaseUtil from "./Utility";
4
5
  import InputBaseUtilityButton from "./Button";
@@ -6,6 +7,7 @@ import { InputStatusIcon } from "./StatusIcon";
6
7
 
7
8
  export const InputFoundation = {
8
9
  Base: InputBase,
10
+ TextArea: InputTextArea,
9
11
  Util: InputBaseUtil,
10
12
  SideSlot: InputBaseSideSlot,
11
13
  Icon: InputStatusIcon,
@@ -277,6 +277,51 @@
277
277
  min-width: 0;
278
278
  }
279
279
 
280
+ .input[data-input-type="textarea"] .input-field {
281
+ // TextArea는 size별로 radius/spacing을 분기해 input과 동일한 스케일 감각을 유지한다.
282
+ --input-textarea-radius: var(--input-textarea-radius-medium);
283
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-medium);
284
+ --input-textarea-padding-block: var(--input-textarea-padding-block-medium);
285
+ --input-textarea-gap: var(--input-textarea-gap-medium);
286
+ align-items: stretch;
287
+ flex-direction: column;
288
+ gap: var(--input-textarea-gap);
289
+ border-radius: var(--input-textarea-radius);
290
+ padding: var(--input-textarea-padding-block)
291
+ var(--input-textarea-padding-inline);
292
+
293
+ &[data-size="small"] {
294
+ --input-textarea-radius: var(--input-textarea-radius-small);
295
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-small);
296
+ --input-textarea-padding-block: var(--input-textarea-padding-block-small);
297
+ --input-textarea-gap: var(--input-textarea-gap-small);
298
+ }
299
+
300
+ &[data-size="large"] {
301
+ --input-textarea-radius: var(--input-textarea-radius-large);
302
+ --input-textarea-padding-inline: var(--input-textarea-padding-inline-large);
303
+ --input-textarea-padding-block: var(--input-textarea-padding-block-large);
304
+ --input-textarea-gap: var(--input-textarea-gap-large);
305
+ }
306
+ }
307
+
308
+ .input-textarea-element {
309
+ min-height: var(--input-textarea-min-height);
310
+ height: var(--input-textarea-height);
311
+ resize: none;
312
+ margin: 0;
313
+ }
314
+
315
+ .input-textarea-length {
316
+ margin: 0;
317
+ align-self: flex-end;
318
+ color: var(--input-placeholder-color);
319
+ font-size: var(--input-textarea-counter-font-size);
320
+ line-height: var(--input-textarea-counter-line-height);
321
+ font-weight: var(--input-textarea-counter-font-weight);
322
+ letter-spacing: var(--input-textarea-counter-letter-spacing);
323
+ }
324
+
280
325
  .input-field-utilities {
281
326
  display: flex;
282
327
  align-items: center;
@@ -421,13 +466,18 @@
421
466
  .input-helper-text {
422
467
  color: var(--input-label-error-color);
423
468
  }
469
+
470
+ .input-textarea-length {
471
+ color: var(--input-label-error-color);
472
+ }
424
473
  }
425
474
 
426
475
  .input[data-state="disabled"] {
427
476
  .input-label,
428
477
  .input-inline-label,
429
478
  .input-helper-text,
430
- .input-affix {
479
+ .input-affix,
480
+ .input-textarea-length {
431
481
  color: var(--input-helper-disabled-color);
432
482
  }
433
483
 
@@ -21,6 +21,27 @@
21
21
  --input-tertiary-radius-base: var(--theme-radius-large-2);
22
22
  --input-table-radius-base: 0;
23
23
  --input-tertiary-element-min-height: var(--theme-size-medium-2);
24
+ --input-textarea-min-height: calc(var(--input-default-height-medium) * 2);
25
+ --input-textarea-height: 128px;
26
+ /* TextArea size token은 Input 기본 size token을 재할당해 축을 일치시킨다. */
27
+ --input-textarea-radius-small: var(--input-default-radius-base);
28
+ --input-textarea-radius-medium: var(--input-default-radius-base);
29
+ --input-textarea-radius-large: var(--input-default-radius-base);
30
+ --input-textarea-padding-inline-small: var(--spacing-padding-6);
31
+ --input-textarea-padding-inline-medium: var(--spacing-padding-6);
32
+ --input-textarea-padding-inline-large: var(--spacing-padding-6);
33
+ --input-textarea-padding-block-small: var(--spacing-padding-5);
34
+ --input-textarea-padding-block-medium: var(--spacing-padding-5);
35
+ --input-textarea-padding-block-large: var(--spacing-padding-6);
36
+ --input-textarea-gap-small: var(--input-default-gap);
37
+ --input-textarea-gap-medium: var(--input-default-gap);
38
+ --input-textarea-gap-large: var(--input-default-gap);
39
+ --input-textarea-counter-font-size: var(--font-label-medium-size);
40
+ --input-textarea-counter-line-height: var(--font-label-medium-line-height);
41
+ --input-textarea-counter-font-weight: var(--font-label-medium-weight);
42
+ --input-textarea-counter-letter-spacing: var(
43
+ --font-label-medium-letter-spacing
44
+ );
24
45
  --input-table-text-small-size: var(--font-body-xxsmall-size);
25
46
  --input-table-text-small-line-height: var(--font-body-xxsmall-line-height);
26
47
  --input-table-text-small-weight: var(--font-body-xxsmall-weight);
@@ -1,4 +1,5 @@
1
1
  export type * from "./foundation";
2
+ export type * from "./textarea";
2
3
  export type * from "./text";
3
4
  export type * from "./date";
4
5
  export type * from "./verification";
@@ -0,0 +1,73 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import type { UseFormRegisterReturn } from "react-hook-form";
3
+ import type { FormFieldWidth } from "../../form/types/props";
4
+ import type { InputPriority, InputSize, InputState } from "./foundation";
5
+
6
+ type NativeTextAreaProps = ComponentPropsWithoutRef<"textarea">;
7
+
8
+ /**
9
+ * Input TextArea; textarea base props
10
+ * @property {NativeTextAreaProps} ... <textarea /> native attrs
11
+ * @property {InputPriority} [priority] 스타일 카테고리 타입
12
+ * @property {InputSize} [size] 스타일 사이즈 타입
13
+ * @property {InputState} [state] input 상태
14
+ * @property {boolean} [block] width: 100% 여부
15
+ * @property {ReactNode} [inlineLabel] tertiary 내부 라벨
16
+ * @property {string} [inputClassName] 실제 `<textarea>` className
17
+ * @property {string} [boxClassName] `.input-box` className
18
+ * @property {UseFormRegisterReturn} [register] react-hook-form register 반환값
19
+ * @property {FormFieldWidth} [width] width preset 옵션
20
+ * @property {InputState} [data-simulated-state] Storybook 시각 상태 강제용
21
+ * @property {number | string} [height] textarea 높이(px 또는 CSS length)
22
+ * @property {number} [length] 카운터 최대 글자수(지정 시 우측 하단 카운터 노출)
23
+ */
24
+ export interface InputTextAreaProps extends Omit<NativeTextAreaProps, "size"> {
25
+ /**
26
+ * semantic color/token 세트
27
+ */
28
+ priority?: InputPriority;
29
+ /**
30
+ * 높이/타이포 세트
31
+ */
32
+ size?: InputSize;
33
+ /**
34
+ * 시각 상태. disabled prop과 조합된다
35
+ */
36
+ state?: InputState;
37
+ /**
38
+ * true면 width:100%
39
+ */
40
+ block?: boolean;
41
+ /**
42
+ * tertiary priority에서 border 내부 상단에 배치할 라벨 텍스트/노드
43
+ */
44
+ inlineLabel?: ReactNode;
45
+ /**
46
+ * 실제 `<textarea>` className
47
+ */
48
+ inputClassName?: string;
49
+ /**
50
+ * `.input-box` className
51
+ */
52
+ boxClassName?: string;
53
+ /**
54
+ * react-hook-form register 반환값
55
+ */
56
+ register?: UseFormRegisterReturn;
57
+ /**
58
+ * width preset 옵션
59
+ */
60
+ width?: FormFieldWidth;
61
+ /**
62
+ * Storybook 등에서 강제 상태 표현용
63
+ */
64
+ "data-simulated-state"?: InputState;
65
+ /**
66
+ * textarea 높이(px 또는 CSS length)
67
+ */
68
+ height?: number | string;
69
+ /**
70
+ * 카운터 최대 글자수(지정 시 우측 하단 카운터 노출)
71
+ */
72
+ length?: number;
73
+ }
@@ -21,12 +21,30 @@ export default function SlotText<C extends ElementType = "span">({
21
21
  return children;
22
22
  }
23
23
 
24
+ // 변경: rest spread로 유입될 수 있는 className 충돌을 제거해 slot-text 클래스가 항상 유지되도록 한다.
25
+ const restPropsRecord = restProps as Record<string, unknown>;
26
+ const restClassName =
27
+ typeof restPropsRecord.className === "string"
28
+ ? restPropsRecord.className
29
+ : undefined;
30
+ const mergedClassName = clsx("slot-text", restClassName, className);
31
+ const normalizedRestProps = { ...restPropsRecord };
32
+ delete normalizedRestProps.className;
33
+
34
+ const hasCustomTitle =
35
+ "title" in normalizedRestProps &&
36
+ typeof normalizedRestProps.title !== "undefined";
37
+
38
+ // 변경: 텍스트 children은 기본 title을 자동 주입해 native tooltip 경로를 보장한다.
39
+ const nativeTitleProps = hasCustomTitle ? {} : { title: String(children) };
40
+
24
41
  // 문자열/숫자 children만 공통 slot text 마크업으로 감싼다.
25
42
  return (
26
43
  <SlotBase
27
44
  as={as as ElementType}
28
- className={clsx("slot-text", className)}
29
- {...restProps}
45
+ className={mergedClassName}
46
+ {...nativeTitleProps}
47
+ {...normalizedRestProps}
30
48
  >
31
49
  {children}
32
50
  </SlotBase>