@uniai-fe/uds-primitives 0.0.1
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 +63 -0
- package/package.json +85 -0
- package/src/components/alternate/hooks/index.ts +4 -0
- package/src/components/alternate/img/.gitkeep +0 -0
- package/src/components/alternate/index.scss +1 -0
- package/src/components/alternate/index.tsx +4 -0
- package/src/components/alternate/markup/index.tsx +4 -0
- package/src/components/alternate/styles/index.scss +3 -0
- package/src/components/alternate/types/index.ts +4 -0
- package/src/components/alternate/utils/index.ts +4 -0
- package/src/components/badge/hooks/index.ts +4 -0
- package/src/components/badge/img/.gitkeep +0 -0
- package/src/components/badge/index.scss +1 -0
- package/src/components/badge/index.tsx +6 -0
- package/src/components/badge/markup/Badge.tsx +51 -0
- package/src/components/badge/markup/index.tsx +1 -0
- package/src/components/badge/styles/index.scss +189 -0
- package/src/components/badge/types/index.ts +55 -0
- package/src/components/badge/utils/index.ts +21 -0
- package/src/components/button/hooks/index.ts +4 -0
- package/src/components/button/img/.gitkeep +0 -0
- package/src/components/button/index.scss +1 -0
- package/src/components/button/index.tsx +6 -0
- package/src/components/button/markup/Button.tsx +175 -0
- package/src/components/button/markup/index.tsx +1 -0
- package/src/components/button/styles/index.scss +847 -0
- package/src/components/button/types/index.ts +79 -0
- package/src/components/button/utils/index.ts +58 -0
- package/src/components/calendar/hooks/index.ts +4 -0
- package/src/components/calendar/img/.gitkeep +0 -0
- package/src/components/calendar/index.scss +1 -0
- package/src/components/calendar/index.tsx +4 -0
- package/src/components/calendar/markup/index.tsx +4 -0
- package/src/components/calendar/styles/index.scss +3 -0
- package/src/components/calendar/types/index.ts +4 -0
- package/src/components/calendar/utils/index.ts +4 -0
- package/src/components/checkbox/hooks/index.ts +4 -0
- package/src/components/checkbox/img/.gitkeep +0 -0
- package/src/components/checkbox/img/check-large.svg +3 -0
- package/src/components/checkbox/img/check-medium.svg +3 -0
- package/src/components/checkbox/img/check.svg +3 -0
- package/src/components/checkbox/index.scss +1 -0
- package/src/components/checkbox/index.tsx +4 -0
- package/src/components/checkbox/markup/Checkbox.tsx +127 -0
- package/src/components/checkbox/markup/index.ts +1 -0
- package/src/components/checkbox/styles/index.scss +164 -0
- package/src/components/checkbox/types/checkbox.ts +21 -0
- package/src/components/checkbox/types/index.ts +1 -0
- package/src/components/chip/hooks/index.ts +4 -0
- package/src/components/chip/img/.gitkeep +0 -0
- package/src/components/chip/img/remove.svg +3 -0
- package/src/components/chip/index.scss +1 -0
- package/src/components/chip/index.tsx +6 -0
- package/src/components/chip/markup/Chip.tsx +103 -0
- package/src/components/chip/markup/index.tsx +1 -0
- package/src/components/chip/styles/index.scss +140 -0
- package/src/components/chip/types/index.ts +52 -0
- package/src/components/chip/utils/index.ts +36 -0
- package/src/components/dialog/hooks/index.ts +4 -0
- package/src/components/dialog/img/.gitkeep +0 -0
- package/src/components/dialog/index.scss +1 -0
- package/src/components/dialog/index.tsx +3 -0
- package/src/components/dialog/markup/confirm-dialog.tsx +316 -0
- package/src/components/dialog/markup/index.tsx +4 -0
- package/src/components/dialog/markup/notice-dialog.tsx +191 -0
- package/src/components/dialog/styles/base.scss +153 -0
- package/src/components/dialog/styles/confirm.scss +58 -0
- package/src/components/dialog/styles/index.scss +3 -0
- package/src/components/dialog/styles/notice.scss +65 -0
- package/src/components/dialog/types/index.ts +70 -0
- package/src/components/dialog/utils/index.ts +4 -0
- package/src/components/drawer/hooks/index.ts +113 -0
- package/src/components/drawer/img/.gitkeep +0 -0
- package/src/components/drawer/img/close.svg +3 -0
- package/src/components/drawer/index.scss +1 -0
- package/src/components/drawer/index.tsx +3 -0
- package/src/components/drawer/markup/drawer.tsx +421 -0
- package/src/components/drawer/markup/index.tsx +3 -0
- package/src/components/drawer/styles/index.scss +232 -0
- package/src/components/drawer/types/index.ts +51 -0
- package/src/components/drawer/utils/context.ts +15 -0
- package/src/components/drawer/utils/index.tsx +77 -0
- package/src/components/dropdown/hooks/index.ts +4 -0
- package/src/components/dropdown/img/.gitkeep +0 -0
- package/src/components/dropdown/index.scss +1 -0
- package/src/components/dropdown/index.tsx +4 -0
- package/src/components/dropdown/markup/index.tsx +4 -0
- package/src/components/dropdown/styles/index.scss +3 -0
- package/src/components/dropdown/types/index.ts +4 -0
- package/src/components/dropdown/utils/index.ts +4 -0
- package/src/components/input/hooks/index.ts +4 -0
- package/src/components/input/img/.gitkeep +0 -0
- package/src/components/input/img/check-correct.svg +3 -0
- package/src/components/input/img/check-default.svg +3 -0
- package/src/components/input/img/check-incorrect.svg +3 -0
- package/src/components/input/img/error.svg +5 -0
- package/src/components/input/img/hide-off.svg +4 -0
- package/src/components/input/img/hide-on.svg +6 -0
- package/src/components/input/img/reset.svg +3 -0
- package/src/components/input/img/search.svg +4 -0
- package/src/components/input/img/success.svg +3 -0
- package/src/components/input/index.scss +1 -0
- package/src/components/input/index.tsx +6 -0
- package/src/components/input/markup/index.tsx +1 -0
- package/src/components/input/markup/text/Base.tsx +311 -0
- package/src/components/input/markup/text/Identification.tsx +145 -0
- package/src/components/input/markup/text/Password.tsx +71 -0
- package/src/components/input/markup/text/Phone.tsx +115 -0
- package/src/components/input/markup/text/Search.tsx +35 -0
- package/src/components/input/markup/text/index.ts +10 -0
- package/src/components/input/styles/index.scss +375 -0
- package/src/components/input/types/index.ts +56 -0
- package/src/components/input/utils/index.ts +54 -0
- package/src/components/label/hooks/index.ts +4 -0
- package/src/components/label/img/.gitkeep +0 -0
- package/src/components/label/index.scss +1 -0
- package/src/components/label/index.tsx +4 -0
- package/src/components/label/markup/index.tsx +4 -0
- package/src/components/label/styles/index.scss +3 -0
- package/src/components/label/types/index.ts +4 -0
- package/src/components/label/utils/index.ts +4 -0
- package/src/components/navigation/hooks/index.ts +4 -0
- package/src/components/navigation/img/.gitkeep +0 -0
- package/src/components/navigation/index.scss +1 -0
- package/src/components/navigation/index.tsx +8 -0
- package/src/components/navigation/markup/index.tsx +2 -0
- package/src/components/navigation/markup/mobile/BottomNavigation.tsx +127 -0
- package/src/components/navigation/markup/mobile/index.ts +1 -0
- package/src/components/navigation/markup/web/index.ts +4 -0
- package/src/components/navigation/styles/index.scss +133 -0
- package/src/components/navigation/types/index.ts +38 -0
- package/src/components/navigation/utils/index.ts +23 -0
- package/src/components/pagination/hooks/index.ts +4 -0
- package/src/components/pagination/img/.gitkeep +0 -0
- package/src/components/pagination/index.scss +1 -0
- package/src/components/pagination/index.tsx +6 -0
- package/src/components/pagination/markup/Carousel.tsx +76 -0
- package/src/components/pagination/markup/Count.tsx +54 -0
- package/src/components/pagination/markup/Pagination.tsx +83 -0
- package/src/components/pagination/markup/index.tsx +3 -0
- package/src/components/pagination/styles/index.scss +155 -0
- package/src/components/pagination/types/index.ts +68 -0
- package/src/components/pagination/utils/index.ts +58 -0
- package/src/components/radio/hooks/index.ts +4 -0
- package/src/components/radio/img/.gitkeep +0 -0
- package/src/components/radio/index.scss +1 -0
- package/src/components/radio/index.tsx +7 -0
- package/src/components/radio/markup/Radio.tsx +121 -0
- package/src/components/radio/markup/RadioCard.tsx +68 -0
- package/src/components/radio/markup/RadioCardGroup.tsx +75 -0
- package/src/components/radio/markup/index.tsx +3 -0
- package/src/components/radio/styles/index.scss +252 -0
- package/src/components/radio/types/index.ts +1 -0
- package/src/components/radio/types/radio.ts +63 -0
- package/src/components/radio/utils/index.ts +4 -0
- package/src/components/scrollbar/hooks/index.ts +4 -0
- package/src/components/scrollbar/img/.gitkeep +0 -0
- package/src/components/scrollbar/index.scss +1 -0
- package/src/components/scrollbar/index.tsx +4 -0
- package/src/components/scrollbar/markup/index.tsx +4 -0
- package/src/components/scrollbar/styles/index.scss +3 -0
- package/src/components/scrollbar/types/index.ts +4 -0
- package/src/components/scrollbar/utils/index.ts +4 -0
- package/src/components/segmented-control/index.scss +1 -0
- package/src/components/segmented-control/index.tsx +7 -0
- package/src/components/segmented-control/markup/SegmentedControl.tsx +117 -0
- package/src/components/segmented-control/markup/index.ts +1 -0
- package/src/components/segmented-control/styles/index.scss +113 -0
- package/src/components/segmented-control/types/index.ts +22 -0
- package/src/components/select/hooks/index.ts +4 -0
- package/src/components/select/img/.gitkeep +0 -0
- package/src/components/select/index.scss +1 -0
- package/src/components/select/index.tsx +4 -0
- package/src/components/select/markup/index.tsx +4 -0
- package/src/components/select/styles/index.scss +3 -0
- package/src/components/select/types/index.ts +4 -0
- package/src/components/select/utils/index.ts +4 -0
- package/src/components/spinner/hooks/index.ts +4 -0
- package/src/components/spinner/img/.gitkeep +0 -0
- package/src/components/spinner/index.scss +1 -0
- package/src/components/spinner/index.tsx +4 -0
- package/src/components/spinner/markup/index.tsx +4 -0
- package/src/components/spinner/styles/index.scss +3 -0
- package/src/components/spinner/types/index.ts +4 -0
- package/src/components/spinner/utils/index.ts +4 -0
- package/src/components/tab/hooks/index.ts +4 -0
- package/src/components/tab/img/.gitkeep +0 -0
- package/src/components/tab/index.scss +1 -0
- package/src/components/tab/index.tsx +6 -0
- package/src/components/tab/markup/TabContent.tsx +29 -0
- package/src/components/tab/markup/TabList.tsx +60 -0
- package/src/components/tab/markup/TabRoot.tsx +74 -0
- package/src/components/tab/markup/TabTrigger.tsx +47 -0
- package/src/components/tab/markup/index.tsx +4 -0
- package/src/components/tab/styles/index.scss +182 -0
- package/src/components/tab/types/index.ts +46 -0
- package/src/components/tab/utils/index.ts +5 -0
- package/src/components/tab/utils/tab-context.ts +20 -0
- package/src/components/table/hooks/index.ts +4 -0
- package/src/components/table/img/.gitkeep +0 -0
- package/src/components/table/index.scss +1 -0
- package/src/components/table/index.tsx +4 -0
- package/src/components/table/markup/index.tsx +4 -0
- package/src/components/table/styles/index.scss +3 -0
- package/src/components/table/types/index.ts +4 -0
- package/src/components/table/utils/index.ts +4 -0
- package/src/hooks/index.ts +4 -0
- package/src/img/.gitkeep +0 -0
- package/src/index.scss +3 -0
- package/src/index.tsx +26 -0
- package/src/init/dayjs.ts +14 -0
- package/src/theme/ThemeProvider.tsx +25 -0
- package/src/theme/config.ts +29 -0
- package/src/theme/index.ts +3 -0
- package/src/theme/overrides.scss +215 -0
- package/src/types/index.ts +4 -0
- package/src/utils/index.ts +4 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cloneElement, isValidElement } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
ForwardedRef,
|
|
5
|
+
JSXElementConstructor,
|
|
6
|
+
ReactElement,
|
|
7
|
+
ReactNode,
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// 다중 ref를 하나의 콜백으로 합쳐주는 유틸.
|
|
11
|
+
const mergeRefs = <T,>(
|
|
12
|
+
...refs: (ForwardedRef<T> | undefined)[]
|
|
13
|
+
): ((node: T | null) => void) => {
|
|
14
|
+
return node => {
|
|
15
|
+
refs.forEach(ref => {
|
|
16
|
+
if (typeof ref === "function") {
|
|
17
|
+
ref(node);
|
|
18
|
+
} else if (ref && typeof ref === "object") {
|
|
19
|
+
(ref as { current: T | null }).current = node;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 사용자 핸들러와 내부 핸들러를 병렬로 실행한다.
|
|
26
|
+
const composeEventHandlers =
|
|
27
|
+
<E extends { defaultPrevented: boolean }>(
|
|
28
|
+
userHandler?: (event: E) => void,
|
|
29
|
+
ourHandler?: (event: E) => void,
|
|
30
|
+
) =>
|
|
31
|
+
(event: E) => {
|
|
32
|
+
userHandler?.(event);
|
|
33
|
+
if (!event.defaultPrevented) {
|
|
34
|
+
ourHandler?.(event);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// asChild 패턴을 지원하는 button 추상화.
|
|
39
|
+
type ButtonClickEvent = Parameters<
|
|
40
|
+
NonNullable<ComponentPropsWithoutRef<"button">["onClick"]>
|
|
41
|
+
>[0];
|
|
42
|
+
|
|
43
|
+
type GenericElement = ReactElement<
|
|
44
|
+
Record<string, unknown>,
|
|
45
|
+
string | JSXElementConstructor<Record<string, unknown>>
|
|
46
|
+
> & {
|
|
47
|
+
ref?: ForwardedRef<HTMLButtonElement>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const renderButtonLike = (
|
|
51
|
+
asChild: boolean | undefined,
|
|
52
|
+
children: ReactNode,
|
|
53
|
+
props: ComponentPropsWithoutRef<"button">,
|
|
54
|
+
ref: ForwardedRef<HTMLButtonElement>,
|
|
55
|
+
) => {
|
|
56
|
+
if (asChild && children && isValidElement(children)) {
|
|
57
|
+
const element = children as GenericElement;
|
|
58
|
+
const mergedOnClick = composeEventHandlers<ButtonClickEvent>(
|
|
59
|
+
element.props.onClick as ((event: ButtonClickEvent) => void) | undefined,
|
|
60
|
+
props.onClick,
|
|
61
|
+
);
|
|
62
|
+
return cloneElement(element, {
|
|
63
|
+
...(props as Record<string, unknown>),
|
|
64
|
+
onClick: mergedOnClick,
|
|
65
|
+
ref: mergeRefs(element.ref, ref),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button type="button" ref={ref} {...props}>
|
|
71
|
+
{children}
|
|
72
|
+
</button>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { composeEventHandlers, mergeRefs, renderButtonLike };
|
|
77
|
+
export * from "./context";
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles/index.scss";
|
|
File without changes
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.9268 6.44141C10.7185 6.23363 10.381 6.23351 10.1729 6.44141L7.48535 9.12891L5.71094 7.35352C5.50266 7.14524 5.16433 7.14524 4.95605 7.35352C4.74799 7.56167 4.74821 7.89914 4.95605 8.10742L7.1084 10.2598C7.31665 10.468 7.655 10.4689 7.86328 10.2607L10.9277 7.19629C11.1358 6.98799 11.135 6.6496 10.9268 6.44141Z" fill="#1AB24D"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.9268 6.44141C10.7185 6.23363 10.381 6.23351 10.1729 6.44141L7.48535 9.12891L5.71094 7.35352C5.50266 7.14524 5.16433 7.14524 4.95605 7.35352C4.74799 7.56167 4.74821 7.89914 4.95605 8.10742L7.1084 10.2598C7.31665 10.468 7.655 10.4689 7.86328 10.2607L10.9277 7.19629C11.1358 6.98799 11.135 6.6496 10.9268 6.44141Z" fill="#CACBCE"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.9268 6.44141C10.7185 6.23363 10.381 6.23351 10.1729 6.44141L7.48535 9.12891L5.71094 7.35352C5.50266 7.14524 5.16433 7.14524 4.95605 7.35352C4.74799 7.56167 4.74821 7.89914 4.95605 8.10742L7.1084 10.2598C7.31665 10.468 7.655 10.4689 7.86328 10.2607L10.9277 7.19629C11.1358 6.98799 11.135 6.6496 10.9268 6.44141Z" fill="#DA1D0B"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="12" cy="12" r="8" fill="#DA1D0B"/>
|
|
3
|
+
<path d="M12 8V13" stroke="white" stroke-width="1.6" stroke-linecap="round"/>
|
|
4
|
+
<circle cx="12" cy="16" r="1" fill="white"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12 6.7998C14.572 6.7998 16.9162 7.73398 18.6162 8.9082C19.4654 9.49478 20.134 10.1272 20.5811 10.707C21.0478 11.3124 21.2002 11.7634 21.2002 12C21.2002 12.2366 21.0478 12.6876 20.5811 13.293C20.134 13.8728 19.4654 14.5052 18.6162 15.0918C16.9162 16.266 14.572 17.2002 12 17.2002C9.428 17.2002 7.08378 16.266 5.38379 15.0918C4.53459 14.5052 3.86603 13.8728 3.41895 13.293C2.95218 12.6876 2.7998 12.2366 2.7998 12C2.7998 11.7634 2.95218 11.3124 3.41895 10.707C3.86603 10.1272 4.53459 9.49478 5.38379 8.9082C7.08378 7.73398 9.428 6.7998 12 6.7998Z" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
3
|
+
<circle cx="12" cy="12" r="3.2" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12 6.7998C14.572 6.7998 16.9162 7.73398 18.6162 8.9082C19.4654 9.49478 20.134 10.1272 20.5811 10.707C21.0478 11.3124 21.2002 11.7634 21.2002 12C21.2002 12.2366 21.0478 12.6876 20.5811 13.293C20.134 13.8728 19.4654 14.5052 18.6162 15.0918C16.9162 16.266 14.572 17.2002 12 17.2002C9.428 17.2002 7.08378 16.266 5.38379 15.0918C4.53459 14.5052 3.86603 13.8728 3.41895 13.293C2.95218 12.6876 2.7998 12.2366 2.7998 12C2.7998 11.7634 2.95218 11.3124 3.41895 10.707C3.86603 10.1272 4.53459 9.49478 5.38379 8.9082C7.08378 7.73398 9.428 6.7998 12 6.7998Z" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
3
|
+
<circle cx="12" cy="12" r="3.2" stroke="#BCBEC2" stroke-width="1.6"/>
|
|
4
|
+
<path d="M20 4L4 20" stroke="white" stroke-width="3.2" stroke-linecap="round"/>
|
|
5
|
+
<path d="M20 4L4 20" stroke="#BCBEC2" stroke-width="1.6" stroke-linecap="round"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3ZM15.04 8.95996C14.7276 8.6477 14.2215 8.64759 13.9092 8.95996L11.999 10.8691L10.0898 8.95996C9.77741 8.6477 9.27135 8.64759 8.95898 8.95996C8.64706 9.27236 8.64687 9.77853 8.95898 10.0908L10.8682 12L8.95898 13.9092C8.64685 14.2215 8.64699 14.7276 8.95898 15.04C9.27135 15.3524 9.77741 15.3523 10.0898 15.04L11.999 13.1299L13.9092 15.04C14.2215 15.3524 14.7276 15.3523 15.04 15.04C15.3523 14.7276 15.3524 14.2215 15.04 13.9092L13.1299 12L15.04 10.0908C15.3524 9.77849 15.3522 9.2724 15.04 8.95996Z" fill="#BCBEC2"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<circle cx="10.5" cy="10.5" r="6.5" stroke="#94989E" stroke-width="1.6" stroke-linecap="round"/>
|
|
3
|
+
<path d="M15.6196 15.6196L19.6196 19.6196" stroke="#94989E" stroke-width="1.6" stroke-linecap="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3ZM16.3906 9.66309C16.0782 9.35097 15.5711 9.35077 15.2588 9.66309L11.2285 13.6934L8.56543 11.0303C8.25302 10.7183 7.74688 10.7181 7.43457 11.0303C7.12215 11.3427 7.12215 11.8497 7.43457 12.1621L10.6631 15.3896C10.9755 15.7021 11.4825 15.703 11.7949 15.3906L16.3906 10.7949C16.7029 10.4826 16.7026 9.97554 16.3906 9.66309Z" fill="#1A6AFF"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@use "./styles/index.scss";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./text";
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import {
|
|
3
|
+
ChangeEvent,
|
|
4
|
+
FocusEvent,
|
|
5
|
+
forwardRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useId,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import type { InputProps } from "../../types";
|
|
13
|
+
import {
|
|
14
|
+
INPUT_AFFIX_CLASSNAME,
|
|
15
|
+
INPUT_ELEMENT_CLASSNAME,
|
|
16
|
+
INPUT_FIELD_CLASSNAME,
|
|
17
|
+
composeInputBoxClassName,
|
|
18
|
+
composeInputClassName,
|
|
19
|
+
} from "../../utils";
|
|
20
|
+
import ErrorIcon from "../../img/error.svg";
|
|
21
|
+
import SuccessIcon from "../../img/success.svg";
|
|
22
|
+
import ResetIcon from "../../img/reset.svg";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Native `<input>` 기반 텍스트 필드. appearance/size/state 축과 label/helper 슬롯을 모두 제공한다.
|
|
26
|
+
* README와 Button 컴포넌트와 동일하게 props를 forwardRef 인자로 직접 구조분해한다.
|
|
27
|
+
*/
|
|
28
|
+
const Text = forwardRef<HTMLInputElement, InputProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
appearance = "primary",
|
|
32
|
+
size = "medium",
|
|
33
|
+
state: stateProp = "default",
|
|
34
|
+
block = false,
|
|
35
|
+
prefix,
|
|
36
|
+
suffix,
|
|
37
|
+
resetSlot,
|
|
38
|
+
successIcon,
|
|
39
|
+
errorIcon,
|
|
40
|
+
label,
|
|
41
|
+
helperText,
|
|
42
|
+
hideHelperText,
|
|
43
|
+
inputClassName,
|
|
44
|
+
wrapperClassName,
|
|
45
|
+
helperTextProps,
|
|
46
|
+
labelProps,
|
|
47
|
+
disabled,
|
|
48
|
+
id,
|
|
49
|
+
className,
|
|
50
|
+
"data-simulated-state": simulatedState,
|
|
51
|
+
type = "text",
|
|
52
|
+
defaultValue,
|
|
53
|
+
value,
|
|
54
|
+
onChange,
|
|
55
|
+
onFocus,
|
|
56
|
+
onBlur,
|
|
57
|
+
...restProps
|
|
58
|
+
},
|
|
59
|
+
forwardedRef,
|
|
60
|
+
) => {
|
|
61
|
+
const generatedId = useId();
|
|
62
|
+
const [uncontrolledValue, setUncontrolledValue] = useState<
|
|
63
|
+
string | number | readonly string[] | undefined
|
|
64
|
+
>(defaultValue);
|
|
65
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
66
|
+
const resolvedState = useMemo(
|
|
67
|
+
() => (disabled ? "disabled" : stateProp),
|
|
68
|
+
[disabled, stateProp],
|
|
69
|
+
);
|
|
70
|
+
const fieldId = useMemo(
|
|
71
|
+
() => id ?? labelProps?.htmlFor ?? generatedId,
|
|
72
|
+
[generatedId, id, labelProps?.htmlFor],
|
|
73
|
+
);
|
|
74
|
+
const shouldShowHelper = Boolean(helperText) && !hideHelperText;
|
|
75
|
+
const helperElementId = useMemo(() => {
|
|
76
|
+
if (!shouldShowHelper) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return helperTextProps?.id ?? `${fieldId}-helper-text`;
|
|
80
|
+
}, [fieldId, helperTextProps?.id, shouldShowHelper]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (stateProp === "disabled" || disabled) {
|
|
84
|
+
setIsFocused(false);
|
|
85
|
+
}
|
|
86
|
+
}, [disabled, stateProp]);
|
|
87
|
+
|
|
88
|
+
const visualState = useMemo(() => {
|
|
89
|
+
if (resolvedState === "disabled") {
|
|
90
|
+
return "disabled";
|
|
91
|
+
}
|
|
92
|
+
if (resolvedState === "error") {
|
|
93
|
+
return "error";
|
|
94
|
+
}
|
|
95
|
+
return isFocused ? "active" : resolvedState;
|
|
96
|
+
}, [isFocused, resolvedState]);
|
|
97
|
+
|
|
98
|
+
const defaultStatusIcon = useMemo(() => {
|
|
99
|
+
if (resolvedState === "success") {
|
|
100
|
+
return <SuccessIcon aria-hidden="true" />;
|
|
101
|
+
}
|
|
102
|
+
if (resolvedState === "error") {
|
|
103
|
+
return <ErrorIcon aria-hidden="true" />;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}, [resolvedState]);
|
|
107
|
+
|
|
108
|
+
const statusSlot = useMemo(() => {
|
|
109
|
+
if (resolvedState === "success") {
|
|
110
|
+
return successIcon ?? defaultStatusIcon;
|
|
111
|
+
}
|
|
112
|
+
if (resolvedState === "error") {
|
|
113
|
+
return errorIcon ?? defaultStatusIcon;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}, [defaultStatusIcon, errorIcon, resolvedState, successIcon]);
|
|
117
|
+
|
|
118
|
+
const defaultResetSlot = useMemo(() => {
|
|
119
|
+
if (visualState === "active") {
|
|
120
|
+
return <ResetIcon aria-hidden="true" />;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}, [visualState]);
|
|
124
|
+
|
|
125
|
+
const effectiveResetSlot = resetSlot ?? defaultResetSlot;
|
|
126
|
+
const currentValue = value ?? uncontrolledValue;
|
|
127
|
+
const hasInputValue =
|
|
128
|
+
currentValue !== undefined && currentValue !== null
|
|
129
|
+
? String(currentValue).length > 0
|
|
130
|
+
: false;
|
|
131
|
+
const showResetSlot = Boolean(
|
|
132
|
+
effectiveResetSlot && hasInputValue && resolvedState !== "disabled",
|
|
133
|
+
);
|
|
134
|
+
const isDisabled = resolvedState === "disabled";
|
|
135
|
+
const labelFor = labelProps?.htmlFor ?? fieldId;
|
|
136
|
+
const containerClassName = useMemo(
|
|
137
|
+
() =>
|
|
138
|
+
composeInputClassName({
|
|
139
|
+
appearance,
|
|
140
|
+
size,
|
|
141
|
+
state: visualState,
|
|
142
|
+
block,
|
|
143
|
+
className,
|
|
144
|
+
}),
|
|
145
|
+
[appearance, block, className, size, visualState],
|
|
146
|
+
);
|
|
147
|
+
const boxClassName = useMemo(
|
|
148
|
+
() =>
|
|
149
|
+
composeInputBoxClassName({
|
|
150
|
+
appearance,
|
|
151
|
+
size,
|
|
152
|
+
state: visualState,
|
|
153
|
+
block,
|
|
154
|
+
className: wrapperClassName,
|
|
155
|
+
}),
|
|
156
|
+
[appearance, block, size, visualState, wrapperClassName],
|
|
157
|
+
);
|
|
158
|
+
const helperClassName = useMemo(
|
|
159
|
+
() => clsx("input-helper-text", helperTextProps?.className),
|
|
160
|
+
[helperTextProps?.className],
|
|
161
|
+
);
|
|
162
|
+
const shouldRenderInlineLabel = appearance === "teritary" && Boolean(label);
|
|
163
|
+
const labelClassName = useMemo(
|
|
164
|
+
() =>
|
|
165
|
+
clsx(
|
|
166
|
+
"input-label",
|
|
167
|
+
shouldRenderInlineLabel && "input-label--visually-hidden",
|
|
168
|
+
labelProps?.className,
|
|
169
|
+
),
|
|
170
|
+
[labelProps?.className, shouldRenderInlineLabel],
|
|
171
|
+
);
|
|
172
|
+
const inputElementClassName = useMemo(
|
|
173
|
+
() => clsx(INPUT_ELEMENT_CLASSNAME, inputClassName),
|
|
174
|
+
[inputClassName],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const handleFocus = useCallback(
|
|
178
|
+
(event: FocusEvent<HTMLInputElement>) => {
|
|
179
|
+
setIsFocused(true);
|
|
180
|
+
onFocus?.(event);
|
|
181
|
+
},
|
|
182
|
+
[onFocus],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const handleBlur = useCallback(
|
|
186
|
+
(event: FocusEvent<HTMLInputElement>) => {
|
|
187
|
+
setIsFocused(false);
|
|
188
|
+
onBlur?.(event);
|
|
189
|
+
},
|
|
190
|
+
[onBlur],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const handleChange = useCallback(
|
|
194
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
195
|
+
if (value === undefined) {
|
|
196
|
+
setUncontrolledValue(event.target.value);
|
|
197
|
+
}
|
|
198
|
+
onChange?.(event);
|
|
199
|
+
},
|
|
200
|
+
[onChange, value],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div
|
|
205
|
+
className={containerClassName}
|
|
206
|
+
data-appearance={appearance}
|
|
207
|
+
data-size={size}
|
|
208
|
+
data-state={visualState}
|
|
209
|
+
data-block={block ? "true" : undefined}
|
|
210
|
+
data-simulated-state={simulatedState}
|
|
211
|
+
>
|
|
212
|
+
{label ? (
|
|
213
|
+
<label
|
|
214
|
+
{...labelProps}
|
|
215
|
+
className={labelClassName}
|
|
216
|
+
htmlFor={labelFor}
|
|
217
|
+
data-slot="label"
|
|
218
|
+
data-appearance={appearance}
|
|
219
|
+
data-state={visualState}
|
|
220
|
+
>
|
|
221
|
+
{label}
|
|
222
|
+
</label>
|
|
223
|
+
) : null}
|
|
224
|
+
<div className={boxClassName} data-slot="box">
|
|
225
|
+
<div
|
|
226
|
+
className={INPUT_FIELD_CLASSNAME}
|
|
227
|
+
data-state={visualState}
|
|
228
|
+
data-appearance={appearance}
|
|
229
|
+
data-size={size}
|
|
230
|
+
data-block={block ? "true" : undefined}
|
|
231
|
+
>
|
|
232
|
+
{shouldRenderInlineLabel ? (
|
|
233
|
+
<div
|
|
234
|
+
className="input-inline-label"
|
|
235
|
+
aria-hidden="true"
|
|
236
|
+
data-slot="inline-label"
|
|
237
|
+
>
|
|
238
|
+
{label}
|
|
239
|
+
</div>
|
|
240
|
+
) : null}
|
|
241
|
+
{prefix ? (
|
|
242
|
+
<div
|
|
243
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--prefix`}
|
|
244
|
+
data-slot="prefix"
|
|
245
|
+
>
|
|
246
|
+
{prefix}
|
|
247
|
+
</div>
|
|
248
|
+
) : null}
|
|
249
|
+
<input
|
|
250
|
+
{...restProps}
|
|
251
|
+
id={fieldId}
|
|
252
|
+
ref={forwardedRef}
|
|
253
|
+
className={inputElementClassName}
|
|
254
|
+
disabled={isDisabled}
|
|
255
|
+
aria-invalid={resolvedState === "error" ? true : undefined}
|
|
256
|
+
aria-describedby={helperElementId}
|
|
257
|
+
type={type}
|
|
258
|
+
defaultValue={defaultValue}
|
|
259
|
+
value={value}
|
|
260
|
+
onChange={handleChange}
|
|
261
|
+
onFocus={handleFocus}
|
|
262
|
+
onBlur={handleBlur}
|
|
263
|
+
/>
|
|
264
|
+
{suffix ? (
|
|
265
|
+
<div
|
|
266
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--suffix`}
|
|
267
|
+
data-slot="suffix"
|
|
268
|
+
>
|
|
269
|
+
{suffix}
|
|
270
|
+
</div>
|
|
271
|
+
) : null}
|
|
272
|
+
{showResetSlot ? (
|
|
273
|
+
<div
|
|
274
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--reset`}
|
|
275
|
+
data-slot="reset"
|
|
276
|
+
data-visible="true"
|
|
277
|
+
>
|
|
278
|
+
{effectiveResetSlot}
|
|
279
|
+
</div>
|
|
280
|
+
) : null}
|
|
281
|
+
{statusSlot ? (
|
|
282
|
+
<div
|
|
283
|
+
className={`${INPUT_AFFIX_CLASSNAME} ${INPUT_AFFIX_CLASSNAME}--status`}
|
|
284
|
+
data-slot="status"
|
|
285
|
+
data-state={resolvedState}
|
|
286
|
+
>
|
|
287
|
+
{statusSlot}
|
|
288
|
+
</div>
|
|
289
|
+
) : null}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
{shouldShowHelper ? (
|
|
293
|
+
<div
|
|
294
|
+
{...helperTextProps}
|
|
295
|
+
className={helperClassName}
|
|
296
|
+
id={helperElementId}
|
|
297
|
+
data-slot="helper-text"
|
|
298
|
+
data-appearance={appearance}
|
|
299
|
+
data-state={visualState}
|
|
300
|
+
>
|
|
301
|
+
{helperText}
|
|
302
|
+
</div>
|
|
303
|
+
) : null}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
Text.displayName = "TextInput";
|
|
310
|
+
|
|
311
|
+
export { Text };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClipboardEvent,
|
|
3
|
+
ChangeEvent,
|
|
4
|
+
forwardRef,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
KeyboardEvent,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import type { InputProps } from "../../types";
|
|
13
|
+
|
|
14
|
+
export interface IdentificationInputProps {
|
|
15
|
+
length?: number;
|
|
16
|
+
label?: InputProps["label"];
|
|
17
|
+
helperText?: InputProps["helperText"];
|
|
18
|
+
state?: InputProps["state"];
|
|
19
|
+
onComplete?: (code: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* IdentificationInput — 인증번호 입력 UI. 개별 입력칸을 제공하고 focus 이동/붙여넣기 등을 처리한다.
|
|
24
|
+
* @param props.length 입력 필드 길이 (4~8 사이).
|
|
25
|
+
* @param props.onComplete 모든 셀이 채워졌을 때 호출.
|
|
26
|
+
*/
|
|
27
|
+
const IdentificationInput = forwardRef<
|
|
28
|
+
HTMLInputElement[],
|
|
29
|
+
IdentificationInputProps
|
|
30
|
+
>(({ length = 6, label, helperText, state, onComplete }, forwardedRef) => {
|
|
31
|
+
const safeLength = Math.max(4, Math.min(8, length));
|
|
32
|
+
const [values, setValues] = useState(() =>
|
|
33
|
+
Array.from({ length: safeLength }, () => ""),
|
|
34
|
+
);
|
|
35
|
+
const inputRefs = useRef<Array<HTMLInputElement | null>>(
|
|
36
|
+
Array(safeLength).fill(null),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const focusCell = useCallback((index: number) => {
|
|
40
|
+
const ref = inputRefs.current[index];
|
|
41
|
+
ref?.focus();
|
|
42
|
+
ref?.select();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const updateValues = useCallback(
|
|
46
|
+
(index: number, digit: string) => {
|
|
47
|
+
setValues(prev => {
|
|
48
|
+
const next = [...prev];
|
|
49
|
+
next[index] = digit;
|
|
50
|
+
if (!next.includes("")) {
|
|
51
|
+
onComplete?.(next.join(""));
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
[onComplete],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const handleChange = useCallback(
|
|
60
|
+
(index: number) => (event: ChangeEvent<HTMLInputElement>) => {
|
|
61
|
+
const digit = event.target.value.replace(/\D/g, "").slice(-1);
|
|
62
|
+
updateValues(index, digit);
|
|
63
|
+
if (digit && index < safeLength - 1) {
|
|
64
|
+
focusCell(index + 1);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[focusCell, safeLength, updateValues],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handleKeyDown = useCallback(
|
|
71
|
+
(index: number) => (event: KeyboardEvent<HTMLInputElement>) => {
|
|
72
|
+
if (event.key === "Backspace" && !values[index] && index > 0) {
|
|
73
|
+
updateValues(index - 1, "");
|
|
74
|
+
focusCell(index - 1);
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[focusCell, updateValues, values],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const handlePaste = useCallback(
|
|
82
|
+
(event: ClipboardEvent<HTMLInputElement>) => {
|
|
83
|
+
const digits = event.clipboardData.getData("text").replace(/\D/g, "");
|
|
84
|
+
if (!digits) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
setValues(prev => {
|
|
89
|
+
const next = [...prev];
|
|
90
|
+
for (let i = 0; i < safeLength; i += 1) {
|
|
91
|
+
next[i] = digits[i] ?? next[i];
|
|
92
|
+
}
|
|
93
|
+
if (!next.includes("")) {
|
|
94
|
+
onComplete?.(next.join(""));
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
[onComplete, safeLength],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const helperNode = useMemo(() => helperText, [helperText]);
|
|
103
|
+
|
|
104
|
+
// forwardRef 사용자는 각 셀 DOM 배열을 직접 제어할 수 있도록 노출한다.
|
|
105
|
+
useImperativeHandle(
|
|
106
|
+
forwardedRef,
|
|
107
|
+
() =>
|
|
108
|
+
inputRefs.current.filter((element): element is HTMLInputElement =>
|
|
109
|
+
Boolean(element),
|
|
110
|
+
),
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="one-time-code" data-state={state}>
|
|
116
|
+
{label ? <div className="one-time-code__label">{label}</div> : null}
|
|
117
|
+
<div className="one-time-code__fields">
|
|
118
|
+
{values.map((value, index) => (
|
|
119
|
+
<input
|
|
120
|
+
key={`identification-${index}`}
|
|
121
|
+
ref={element => {
|
|
122
|
+
inputRefs.current[index] = element;
|
|
123
|
+
}}
|
|
124
|
+
type="text"
|
|
125
|
+
inputMode="numeric"
|
|
126
|
+
className="one-time-code__input"
|
|
127
|
+
maxLength={1}
|
|
128
|
+
value={value}
|
|
129
|
+
onChange={handleChange(index)}
|
|
130
|
+
onKeyDown={handleKeyDown(index)}
|
|
131
|
+
onPaste={handlePaste}
|
|
132
|
+
aria-label={`${index + 1}번째 인증번호 숫자`}
|
|
133
|
+
/>
|
|
134
|
+
))}
|
|
135
|
+
</div>
|
|
136
|
+
{helperNode ? (
|
|
137
|
+
<div className="one-time-code__helper">{helperNode}</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
IdentificationInput.displayName = "IdentificationInput";
|
|
144
|
+
|
|
145
|
+
export { IdentificationInput };
|