@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.
- package/README.md +9 -1
- package/dist/styles.css +130 -35
- package/package.json +2 -2
- package/src/components/checkbox/img/check-large.svg +1 -1
- package/src/components/checkbox/img/check-medium.svg +1 -1
- package/src/components/checkbox/markup/Checkbox.tsx +6 -3
- package/src/components/checkbox/styles/index.scss +38 -25
- package/src/components/input/markup/text/AuthCode.tsx +145 -0
- package/src/components/input/markup/text/Base.tsx +71 -58
- package/src/components/input/markup/text/{EmailVerification.tsx → Email.tsx} +50 -31
- package/src/components/input/markup/text/InputUtilityButton.tsx +46 -0
- package/src/components/input/markup/text/Phone.tsx +65 -7
- package/src/components/input/markup/text/index.ts +4 -4
- package/src/components/input/styles/index.scss +104 -13
- package/src/components/input/types/index.ts +1 -0
- package/src/components/input/markup/text/Identification.tsx +0 -159
|
@@ -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
|
-
|
|
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-
|
|
128
|
-
|
|
129
|
-
margin-left:
|
|
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:
|
|
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
|
-
.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
}
|
|
@@ -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 };
|