@uniai-fe/uds-primitives 0.0.16 → 0.0.18

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.
@@ -12,6 +12,8 @@
12
12
  --theme-input-radius-tertiary: var(--theme-radius-large-2);
13
13
  --theme-input-label-color: var(--color-label-standard);
14
14
  --theme-input-helper-color: var(--color-label-neutral);
15
+ --theme-input-helper-success-color: var(--color-success);
16
+ --theme-input-helper-error-color: var(--color-error);
15
17
  --theme-input-helper-disabled-color: var(--color-label-disabled);
16
18
  --theme-input-label-accent-color: var(--color-primary-default);
17
19
  --theme-input-label-error-color: var(--color-error);
@@ -104,18 +106,52 @@
104
106
  padding-inline: 0;
105
107
  padding-block: var(--spacing-padding-4);
106
108
  background-color: transparent;
109
+
110
+ &[data-state="active"],
111
+ &[data-state="focused"] {
112
+ border-bottom-color: var(--theme-input-border-active);
113
+ border-bottom-width: var(--theme-input-border-width-emphasis);
114
+ }
115
+
116
+ &[data-state="success"] {
117
+ border-bottom-color: var(--theme-input-border-success);
118
+ border-bottom-width: var(--theme-input-border-width-emphasis);
119
+ }
120
+
121
+ &[data-state="error"] {
122
+ border-bottom-color: var(--theme-input-border-error);
123
+ border-bottom-width: var(--theme-input-border-width-emphasis);
124
+ }
125
+
126
+ &[data-state="disabled"] {
127
+ border-bottom-color: var(--theme-input-border-underline-disabled);
128
+ border-bottom-width: var(--theme-input-border-width-default);
129
+ }
107
130
  }
108
131
 
109
132
  &[data-priority="tertiary"] {
110
133
  border-radius: var(--theme-input-radius-tertiary);
111
134
  background-color: var(--theme-input-surface);
112
135
  min-height: var(--theme-input-height-tertiary);
113
- flex-wrap: wrap;
114
136
  row-gap: var(--spacing-gap-1);
115
137
  column-gap: var(--theme-input-gap);
138
+ flex-wrap: wrap;
139
+ align-items: center;
140
+
141
+ .input-field__control {
142
+ display: grid;
143
+ grid-template-columns: auto minmax(0, 1fr);
144
+ column-gap: var(--theme-input-gap);
145
+ row-gap: var(--spacing-gap-1);
146
+ align-items: center;
147
+ flex: 1 1 auto;
148
+ min-width: 0;
149
+ }
116
150
 
117
151
  .input-inline-label {
118
- flex-basis: 100%;
152
+ grid-column: 1 / -1;
153
+ margin: 0;
154
+ align-self: flex-start;
119
155
  }
120
156
 
121
157
  .input-element {
@@ -124,9 +160,9 @@
124
160
  flex: 1 1 auto;
125
161
  }
126
162
 
127
- .input-element + .input-affix {
128
- // inline label 다음 줄에서 input 오른쪽 끝으로 아이콘을 밀어낸다.
129
- margin-left: auto;
163
+ .input-field__utilities {
164
+ align-self: center;
165
+ margin-left: 0;
130
166
  }
131
167
  }
132
168
 
@@ -188,6 +224,26 @@
188
224
  }
189
225
  }
190
226
 
227
+ .input-field__control {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: var(--theme-input-gap);
231
+ flex: 1 1 auto;
232
+ min-width: 0;
233
+ }
234
+
235
+ .input-field__utilities {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: var(--spacing-gap-2, 8px);
239
+ flex-shrink: 0;
240
+ margin-left: var(--spacing-gap-3, 12px);
241
+
242
+ .input-affix {
243
+ margin-left: 0;
244
+ }
245
+ }
246
+
191
247
  .input-inline-label {
192
248
  order: -2;
193
249
  flex-basis: 100%;
@@ -213,13 +269,17 @@
213
269
  color: var(--theme-input-border-error);
214
270
  }
215
271
 
272
+ // [data-state="success"] & {
273
+ // color: var(--theme-input-helper-success-color);
274
+ // }
275
+
216
276
  [data-state="disabled"] & {
217
277
  color: var(--theme-input-helper-disabled-color);
218
278
  }
219
279
  }
220
280
 
221
281
  .input-affix {
222
- display: inline-flex;
282
+ display: flex;
223
283
  align-items: center;
224
284
  justify-content: center;
225
285
  min-width: 20px;
@@ -313,8 +373,7 @@
313
373
  }
314
374
  }
315
375
 
316
- .input-password-toggle,
317
- .input-action-button {
376
+ .input-password-toggle {
318
377
  border: none;
319
378
  background: transparent;
320
379
  color: var(--theme-input-label-accent-color);
@@ -374,14 +433,46 @@
374
433
  color: var(--theme-input-helper-color);
375
434
  }
376
435
 
377
- .email-verification {
436
+ .email-verification,
437
+ .phone-verification {
378
438
  display: flex;
379
439
  flex-direction: column;
380
440
  gap: var(--spacing-gap-4);
381
441
  }
382
442
 
383
- .email-verification__countdown {
384
- font-size: var(--font-caption-medium-size);
385
- line-height: var(--font-caption-medium-line-height);
386
- color: var(--theme-input-helper-color);
443
+ .auth-code-input__actions,
444
+ .email-verification__code-actions,
445
+ .phone-verification__code-actions {
446
+ display: flex;
447
+ align-items: center;
448
+ justify-content: flex-end;
449
+ gap: var(--spacing-gap-3);
450
+ min-width: 0;
451
+ }
452
+
453
+ .auth-code-input__countdown,
454
+ .email-verification__countdown,
455
+ .phone-verification__countdown {
456
+ display: flex;
457
+ align-items: center;
458
+ font-weight: 500;
459
+ font-style: normal;
460
+ font-size: 13px;
461
+ line-height: 1em;
462
+ letter-spacing: -0.0025em;
463
+ color: var(--color-primary-default);
464
+ flex-shrink: 0;
465
+ }
466
+
467
+ .button.input-utility-button {
468
+ min-height: 32px;
469
+ padding: var(--spacing-padding-2, 4px) var(--spacing-padding-6, 24px);
470
+ border-radius: var(--shape-rounded-1, 8px);
471
+
472
+ .button-label {
473
+ font-size: var(--font-body-xxsmall-size);
474
+ line-height: var(--font-body-xxsmall-line-height);
475
+ letter-spacing: var(--font-body-xxsmall-letter-spacing);
476
+ font-weight: var(--font-body-xxsmall-weight);
477
+ }
387
478
  }
@@ -19,6 +19,7 @@ export const INPUT_STATES = [
19
19
  "success",
20
20
  "error",
21
21
  "disabled",
22
+ "loading",
22
23
  ] as const;
23
24
 
24
25
  export type InputPriority = (typeof INPUT_PRIORITIES)[number];
@@ -1,159 +0,0 @@
1
- import {
2
- ClipboardEvent,
3
- ChangeEvent,
4
- forwardRef,
5
- useImperativeHandle,
6
- KeyboardEvent,
7
- ReactNode,
8
- useCallback,
9
- useMemo,
10
- useRef,
11
- useState,
12
- } from "react";
13
- import type { InputState } from "../../types";
14
-
15
- /**
16
- * IdentificationInput props. 고정 길이 숫자 코드 입력에 필요한 label/helper/state/onComplete를 제공한다.
17
- * @property {number} [length=6] 입력칸 개수(4~8 사이로 자동 보정).
18
- * @property {ReactNode} [label] 상단 라벨.
19
- * @property {ReactNode} [helper] helper 텍스트.
20
- * @property {InputState} [state="default"] 시각 상태.
21
- * @property {(code: string) => void} [onComplete] 모든 셀이 채워졌을 때 호출.
22
- */
23
- export interface IdentificationInputProps {
24
- length?: number;
25
- label?: ReactNode;
26
- helper?: ReactNode;
27
- state?: InputState;
28
- onComplete?: (code: string) => void;
29
- }
30
-
31
- /**
32
- * IdentificationInput — 인증번호 입력 UI. 개별 입력칸을 제공하고 focus 이동/붙여넣기 등을 처리한다.
33
- * @component
34
- * @param {IdentificationInputProps} props
35
- * @param {number} [props.length=6] 입력 필드 길이. 4~8 범위로 자동 보정된다.
36
- * @param {ReactNode} [props.label] 상단 label 콘텐츠.
37
- * @param {ReactNode} [props.helper] helper 텍스트.
38
- * @param {InputState} [props.state] 시각 상태.
39
- * @param {(code: string) => void} [props.onComplete] 모든 셀이 채워졌을 때 호출되는 콜백.
40
- */
41
- const IdentificationInput = forwardRef<
42
- HTMLInputElement[],
43
- IdentificationInputProps
44
- >(({ length = 6, label, helper, state, onComplete }, forwardedRef) => {
45
- const safeLength = Math.max(4, Math.min(8, length));
46
- const [values, setValues] = useState(() =>
47
- Array.from({ length: safeLength }, () => ""),
48
- );
49
- const inputRefs = useRef<Array<HTMLInputElement | null>>(
50
- Array(safeLength).fill(null),
51
- );
52
-
53
- const focusCell = useCallback((index: number) => {
54
- const ref = inputRefs.current[index];
55
- ref?.focus();
56
- ref?.select();
57
- }, []);
58
-
59
- const updateValues = useCallback(
60
- (index: number, digit: string) => {
61
- setValues(prev => {
62
- const next = [...prev];
63
- next[index] = digit;
64
- if (!next.includes("")) {
65
- onComplete?.(next.join(""));
66
- }
67
- return next;
68
- });
69
- },
70
- [onComplete],
71
- );
72
-
73
- const handleChange = useCallback(
74
- (index: number) => (event: ChangeEvent<HTMLInputElement>) => {
75
- const digit = event.target.value.replace(/\D/g, "").slice(-1);
76
- updateValues(index, digit);
77
- if (digit && index < safeLength - 1) {
78
- focusCell(index + 1);
79
- }
80
- },
81
- [focusCell, safeLength, updateValues],
82
- );
83
-
84
- const handleKeyDown = useCallback(
85
- (index: number) => (event: KeyboardEvent<HTMLInputElement>) => {
86
- if (event.key === "Backspace" && !values[index] && index > 0) {
87
- updateValues(index - 1, "");
88
- focusCell(index - 1);
89
- event.preventDefault();
90
- }
91
- },
92
- [focusCell, updateValues, values],
93
- );
94
-
95
- const handlePaste = useCallback(
96
- (event: ClipboardEvent<HTMLInputElement>) => {
97
- const digits = event.clipboardData.getData("text").replace(/\D/g, "");
98
- if (!digits) {
99
- return;
100
- }
101
- event.preventDefault();
102
- setValues(prev => {
103
- const next = [...prev];
104
- for (let i = 0; i < safeLength; i += 1) {
105
- next[i] = digits[i] ?? next[i];
106
- }
107
- if (!next.includes("")) {
108
- onComplete?.(next.join(""));
109
- }
110
- return next;
111
- });
112
- },
113
- [onComplete, safeLength],
114
- );
115
-
116
- const helperNode = useMemo(() => helper, [helper]);
117
-
118
- // forwardRef 사용자는 각 셀 DOM 배열을 직접 제어할 수 있도록 노출한다.
119
- useImperativeHandle(
120
- forwardedRef,
121
- () =>
122
- inputRefs.current.filter((element): element is HTMLInputElement =>
123
- Boolean(element),
124
- ),
125
- [],
126
- );
127
-
128
- return (
129
- <div className="one-time-code" data-state={state}>
130
- {label ? <div className="one-time-code__label">{label}</div> : null}
131
- <div className="one-time-code__fields">
132
- {values.map((value, index) => (
133
- <input
134
- key={`identification-${index}`}
135
- ref={element => {
136
- inputRefs.current[index] = element;
137
- }}
138
- type="text"
139
- inputMode="numeric"
140
- className="one-time-code__input"
141
- maxLength={1}
142
- value={value}
143
- onChange={handleChange(index)}
144
- onKeyDown={handleKeyDown(index)}
145
- onPaste={handlePaste}
146
- aria-label={`${index + 1}번째 인증번호 숫자`}
147
- />
148
- ))}
149
- </div>
150
- {helperNode ? (
151
- <div className="one-time-code__helper">{helperNode}</div>
152
- ) : null}
153
- </div>
154
- );
155
- });
156
-
157
- IdentificationInput.displayName = "IdentificationInput";
158
-
159
- export { IdentificationInput };