@uniai-fe/uds-primitives 0.0.15 → 0.0.17
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 +139 -35
- package/package.json +1 -1
- 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 +63 -57
- 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 +98 -13
- package/src/components/pagination/markup/Carousel.tsx +71 -53
- package/src/components/pagination/markup/Count.tsx +9 -6
- package/src/components/pagination/markup/Pagination.tsx +11 -9
- package/src/components/pagination/styles/index.scss +12 -0
- package/src/components/pagination/types/index.ts +17 -4
- package/src/components/input/markup/text/Identification.tsx +0 -159
|
@@ -9,68 +9,86 @@ import {
|
|
|
9
9
|
} from "../utils";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* @component PaginationCarousel
|
|
13
|
+
* @description
|
|
14
|
+
* Carousel(dot) indicator를 렌더한다. total/current는 normalizePaginationState로 보정하고,
|
|
15
|
+
* onPageChange가 없으면 버튼을 disabled 처리해 pointer/cursor를 제거한다.
|
|
16
|
+
* priority prop은 data attribute로 노출되어 SCSS에서 primary/secondary(active color) 토큰을 분기한다.
|
|
17
|
+
* @param {PaginationCarouselProps} props Carousel 구성 옵션.
|
|
18
|
+
* @param {number} props.total 전체 step 개수.
|
|
16
19
|
* @param {number} [props.current=1] 현재 step index(1-indexed).
|
|
17
|
-
* @param {(page: number) => void} [props.onPageChange]
|
|
18
|
-
*
|
|
19
|
-
* @param {
|
|
20
|
+
* @param {(page: number) => void} [props.onPageChange] dot를 클릭해 이동시키는 핸들러. 미제공 시 인터랙션이 비활성화된다.
|
|
21
|
+
* @param {string} [props.className] `.pagination` 루트 className merge 용도.
|
|
22
|
+
* @param {PaginationCarouselProps["priority"]} [props.priority=\"primary\"] active dot 색상 priority.
|
|
23
|
+
* @returns {JSX.Element} dot indicator `<ul>`.
|
|
20
24
|
*/
|
|
21
25
|
const PaginationCarousel = forwardRef<
|
|
22
26
|
HTMLUListElement,
|
|
23
27
|
PaginationCarouselProps
|
|
24
|
-
>(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
total,
|
|
32
|
+
current = 1,
|
|
33
|
+
onPageChange,
|
|
34
|
+
className,
|
|
35
|
+
priority = "primary",
|
|
36
|
+
...restProps
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) => {
|
|
40
|
+
const { total: normalizedTotal, current: normalizedCurrent } =
|
|
41
|
+
normalizePaginationState({ total, current });
|
|
42
|
+
const pages = createPaginationPages(normalizedTotal);
|
|
43
|
+
const allowInteraction = typeof onPageChange === "function";
|
|
44
|
+
const rootClassName = composePaginationClassName({
|
|
45
|
+
variant: "carousel",
|
|
46
|
+
className,
|
|
47
|
+
});
|
|
33
48
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
const handleClick = (page: number) => {
|
|
50
|
+
if (!allowInteraction || page === normalizedCurrent) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
onPageChange?.(page);
|
|
54
|
+
};
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<button
|
|
58
|
-
type="button"
|
|
59
|
-
className={PAGINATION_BUTTON_CLASSNAME}
|
|
56
|
+
// priority data attr는 primary/secondary dot 색상을 분기한다.
|
|
57
|
+
return (
|
|
58
|
+
<ul
|
|
59
|
+
{...restProps}
|
|
60
|
+
ref={ref}
|
|
61
|
+
className={rootClassName}
|
|
62
|
+
data-variant="carousel"
|
|
63
|
+
data-interactive={allowInteraction ? "true" : "false"}
|
|
64
|
+
data-priority={priority}
|
|
65
|
+
>
|
|
66
|
+
{pages.map(page => {
|
|
67
|
+
const isActive = page === normalizedCurrent;
|
|
68
|
+
return (
|
|
69
|
+
<li
|
|
70
|
+
key={page}
|
|
71
|
+
className={PAGINATION_ITEM_CLASSNAME}
|
|
60
72
|
data-active={isActive ? "true" : undefined}
|
|
61
|
-
aria-label={`Step ${page}`}
|
|
62
|
-
disabled={!allowInteraction}
|
|
63
|
-
tabIndex={allowInteraction ? 0 : -1}
|
|
64
|
-
onClick={allowInteraction ? () => handleClick(page) : undefined}
|
|
65
73
|
>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
className={PAGINATION_BUTTON_CLASSNAME}
|
|
77
|
+
data-active={isActive ? "true" : undefined}
|
|
78
|
+
aria-label={`Step ${page}`}
|
|
79
|
+
disabled={!allowInteraction}
|
|
80
|
+
tabIndex={allowInteraction ? 0 : -1}
|
|
81
|
+
onClick={allowInteraction ? () => handleClick(page) : undefined}
|
|
82
|
+
>
|
|
83
|
+
<span className="pagination-dot" aria-hidden="true" />
|
|
84
|
+
</button>
|
|
85
|
+
</li>
|
|
86
|
+
);
|
|
87
|
+
})}
|
|
88
|
+
</ul>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
);
|
|
74
92
|
|
|
75
93
|
PaginationCarousel.displayName = "PaginationCarousel";
|
|
76
94
|
|
|
@@ -7,13 +7,16 @@ import {
|
|
|
7
7
|
} from "../utils";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @
|
|
12
|
-
*
|
|
10
|
+
* @component PaginationCount
|
|
11
|
+
* @description
|
|
12
|
+
* Count(step) indicator를 `current/total` pill 형태로 렌더한다. 실제로는 disabled button을 사용해
|
|
13
|
+
* 시각적 일관성을 유지하고, normalizePaginationState로 total/current를 보정한다.
|
|
14
|
+
* @param {PaginationCountProps} props Count variant 구성 옵션.
|
|
13
15
|
* @param {number} props.total 전체 step 수.
|
|
14
|
-
* @param {number} [props.current=1] 현재
|
|
15
|
-
* @param {"
|
|
16
|
-
* @param {string} [props.className] `.pagination`
|
|
16
|
+
* @param {number} [props.current=1] 현재 위치(1-indexed).
|
|
17
|
+
* @param {PaginationCountProps["size"]} [props.size=\"small\"] 높이/타이포 크기.
|
|
18
|
+
* @param {string} [props.className] `.pagination` 루트 className merge 용도.
|
|
19
|
+
* @returns {JSX.Element} count indicator `<div>`.
|
|
17
20
|
*/
|
|
18
21
|
const PaginationCount = forwardRef<HTMLDivElement, PaginationCountProps>(
|
|
19
22
|
({ total, current = 1, size = "small", className, ...restProps }, ref) => {
|
|
@@ -9,15 +9,17 @@ import {
|
|
|
9
9
|
} from "../utils";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {
|
|
18
|
-
*
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
12
|
+
* @component Pagination
|
|
13
|
+
* @description
|
|
14
|
+
* List variant pagination을 구현한다. total/current 값을 보정해 최소 1~total 범위의 페이지 버튼을 렌더하고,
|
|
15
|
+
* onPageChange가 없으면 data-interactive 값을 false로 지정해 전부 disabled 상태로 노출한다.
|
|
16
|
+
* @param {PaginationProps} props Pagination 구성 옵션.
|
|
17
|
+
* @param {number} props.total 전체 페이지 개수. 1 미만이면 자동으로 1로 보정된다.
|
|
18
|
+
* @param {number} [props.current=1] 현재 활성 페이지(1-indexed). total 범위를 벗어나면 자동 조정된다.
|
|
19
|
+
* @param {(page: number) => void} [props.onPageChange] 페이지 변경 핸들러. 미제공 시 버튼은 disabled 처리되고 tabIndex가 -1이 된다.
|
|
20
|
+
* @param {string} [props.className] `.pagination` 루트 className merge 용도.
|
|
21
|
+
* @param {React.AriaAttributes} [props.aria-*] ARIA 속성은 그대로 `<ul>`에 전달된다.
|
|
22
|
+
* @returns {JSX.Element} 페이지 네비게이터 `<ul>`.
|
|
21
23
|
* @example
|
|
22
24
|
* ```tsx
|
|
23
25
|
* <Pagination total={10} current={3} onPageChange={setPage} />
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
--pagination-dot-size: 8px;
|
|
14
14
|
--pagination-dot-bg: var(--color-cool-gray-85, #d2d3d7);
|
|
15
15
|
--pagination-dot-active-bg: var(--color-primary-default, #0061ff);
|
|
16
|
+
--pagination-dot-active-bg-secondary: var(
|
|
17
|
+
--color-bg-surface-heavy,
|
|
18
|
+
#313235
|
|
19
|
+
); // semantic surface heavy
|
|
16
20
|
--pagination-carousel-height: 8px;
|
|
17
21
|
--pagination-carousel-dot-width: 8px;
|
|
18
22
|
--pagination-carousel-active-width: 20px;
|
|
@@ -94,6 +98,14 @@
|
|
|
94
98
|
align-items: center;
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
// secondary priority일 때 active dot 컬러를 재정의한다.
|
|
102
|
+
.pagination--variant-carousel[data-priority="secondary"] {
|
|
103
|
+
--pagination-dot-active-bg: var(
|
|
104
|
+
--pagination-dot-active-bg-secondary,
|
|
105
|
+
var(--color-secondary-strong, #ccdeff)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
.pagination--variant-carousel .pagination-button {
|
|
98
110
|
width: auto;
|
|
99
111
|
height: var(--pagination-carousel-height);
|
|
@@ -2,9 +2,12 @@ import type { ComponentPropsWithoutRef } from "react";
|
|
|
2
2
|
|
|
3
3
|
export const PAGINATION_VARIANTS = ["list", "carousel", "count"] as const;
|
|
4
4
|
export const PAGINATION_COUNT_SIZES = ["small", "xsmall"] as const;
|
|
5
|
+
export const PAGINATION_CAROUSEL_PRIORITIES = ["primary", "secondary"] as const;
|
|
5
6
|
|
|
6
7
|
export type PaginationVariant = (typeof PAGINATION_VARIANTS)[number];
|
|
7
8
|
export type PaginationCountSize = (typeof PAGINATION_COUNT_SIZES)[number];
|
|
9
|
+
export type PaginationCarouselPriority =
|
|
10
|
+
(typeof PAGINATION_CAROUSEL_PRIORITIES)[number];
|
|
8
11
|
|
|
9
12
|
type NativeListProps = ComponentPropsWithoutRef<"ul">;
|
|
10
13
|
type NativeDivProps = ComponentPropsWithoutRef<"div">;
|
|
@@ -28,7 +31,9 @@ interface PaginationInteractiveProps {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* 숫자 페이지용 Pagination props
|
|
34
|
+
* 숫자 페이지용 Pagination props.
|
|
35
|
+
* native `<ul>` 속성과 state/interaction 옵션(total/current/onPageChange)을 함께 받는다.
|
|
36
|
+
* onPageChange가 전달되지 않으면 버튼이 disabled 상태가 되며 focus 방지를 위해 tabIndex가 -1로 설정된다.
|
|
32
37
|
*/
|
|
33
38
|
export interface PaginationProps
|
|
34
39
|
extends
|
|
@@ -37,16 +42,24 @@ export interface PaginationProps
|
|
|
37
42
|
PaginationInteractiveProps {}
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
|
-
* Carousel
|
|
45
|
+
* Carousel(dot) indicator props.
|
|
46
|
+
* native `<ul>` 속성과 total/current/onPageChange + priority 옵션을 함께 받아 dot 상태를 제어한다.
|
|
47
|
+
* priority는 SCSS data attribute 기반으로 semantic primary/secondary active 색상을 선택한다.
|
|
41
48
|
*/
|
|
42
49
|
export interface PaginationCarouselProps
|
|
43
50
|
extends
|
|
44
51
|
Omit<NativeListProps, "children" | "onChange">,
|
|
45
52
|
PaginationBaseProps,
|
|
46
|
-
PaginationInteractiveProps {
|
|
53
|
+
PaginationInteractiveProps {
|
|
54
|
+
/**
|
|
55
|
+
* dot active 색상 priority. primary(semantic primary standard) 또는 secondary(semantic surface heavy) 옵션을 제공한다.
|
|
56
|
+
*/
|
|
57
|
+
priority?: PaginationCarouselPriority;
|
|
58
|
+
}
|
|
47
59
|
|
|
48
60
|
/**
|
|
49
|
-
* Count(step) indicator props
|
|
61
|
+
* Count(step) indicator props.
|
|
62
|
+
* `<div>` 래퍼에 disabled button을 내장해 `current/total` 텍스트만 출력하며 size로 높이/타이포를 전환한다.
|
|
50
63
|
*/
|
|
51
64
|
export interface PaginationCountProps
|
|
52
65
|
extends Omit<NativeDivProps, "children">, PaginationBaseProps {
|
|
@@ -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 };
|