@uniai-fe/uds-primitives 0.0.14 → 0.0.16
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/dist/styles.css +29 -267
- package/package.json +1 -1
- package/src/components/input/markup/text/EmailVerification.tsx +144 -0
- package/src/components/input/markup/text/Phone.tsx +12 -6
- package/src/components/input/markup/text/index.ts +2 -0
- package/src/components/input/styles/index.scss +12 -0
- 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/index.scss +0 -1
- package/src/index.tsx +0 -1
- package/src/types/form-field.ts +3 -3
package/dist/styles.css
CHANGED
|
@@ -721,14 +721,18 @@
|
|
|
721
721
|
--font-body-medium-line-height: 1.5em;
|
|
722
722
|
--font-body-medium-letter-spacing: 0px;
|
|
723
723
|
--font-body-medium-weight: 500;
|
|
724
|
-
--font-body-small-size:
|
|
724
|
+
--font-body-small-size: 16px;
|
|
725
725
|
--font-body-small-line-height: 1.5em;
|
|
726
726
|
--font-body-small-letter-spacing: 0px;
|
|
727
727
|
--font-body-small-weight: 400;
|
|
728
|
-
--font-body-xsmall-size:
|
|
728
|
+
--font-body-xsmall-size: 15px;
|
|
729
729
|
--font-body-xsmall-line-height: 1.5em;
|
|
730
730
|
--font-body-xsmall-letter-spacing: 0px;
|
|
731
731
|
--font-body-xsmall-weight: 400;
|
|
732
|
+
--font-body-xxsmall-size: 13px;
|
|
733
|
+
--font-body-xxsmall-line-height: 1.5em;
|
|
734
|
+
--font-body-xxsmall-letter-spacing: 0px;
|
|
735
|
+
--font-body-xxsmall-weight: 400;
|
|
732
736
|
}
|
|
733
737
|
}
|
|
734
738
|
@layer theme.tokens.typography {
|
|
@@ -1928,271 +1932,6 @@ figure.chip {
|
|
|
1928
1932
|
height: 100%;
|
|
1929
1933
|
}
|
|
1930
1934
|
|
|
1931
|
-
:where(.radix-themes, .theme-root, :root) {
|
|
1932
|
-
/* dialog essentials */
|
|
1933
|
-
--dialog-overlay-bg: rgba(5, 6, 12, 0.55);
|
|
1934
|
-
--dialog-panel-width: 360px;
|
|
1935
|
-
--dialog-panel-max-width: calc(100vw - var(--spacing-padding-10) * 2);
|
|
1936
|
-
--dialog-panel-bg: var(--color-bg-surface-static-white);
|
|
1937
|
-
--dialog-panel-radius: var(--theme-radius-large-1);
|
|
1938
|
-
--dialog-panel-shadow: 0px 18px 40px rgba(8, 11, 30, 0.18);
|
|
1939
|
-
--dialog-border-color: var(--color-border-standard-cool-gray);
|
|
1940
|
-
--dialog-title-color: var(--color-label-strong);
|
|
1941
|
-
--dialog-title-font-size: var(--font-heading-xsmall-size);
|
|
1942
|
-
--dialog-title-line-height: var(--font-heading-xsmall-line-height);
|
|
1943
|
-
--dialog-title-weight: var(--font-heading-xsmall-weight);
|
|
1944
|
-
--dialog-body-color: var(--color-label-standard);
|
|
1945
|
-
--dialog-body-font-size: var(--font-body-small-size);
|
|
1946
|
-
--dialog-body-line-height: var(--font-body-small-line-height);
|
|
1947
|
-
--dialog-description-color: var(--color-label-standard);
|
|
1948
|
-
--dialog-description-font-size: var(--font-body-small-size);
|
|
1949
|
-
--dialog-description-line-height: var(--font-body-small-line-height);
|
|
1950
|
-
/* alert(notice) defaults */
|
|
1951
|
-
--notice-dialog-section-padding-x: var(--spacing-padding-8);
|
|
1952
|
-
--notice-dialog-section-padding-y: var(--spacing-padding-10);
|
|
1953
|
-
--notice-dialog-action-height: 52px;
|
|
1954
|
-
/* confirm defaults */
|
|
1955
|
-
--confirm-dialog-header-padding-x: var(--spacing-padding-6);
|
|
1956
|
-
--confirm-dialog-header-padding-y: var(--spacing-padding-7);
|
|
1957
|
-
--confirm-dialog-body-padding-x: var(--spacing-padding-6);
|
|
1958
|
-
--confirm-dialog-body-padding-y: var(--spacing-padding-7);
|
|
1959
|
-
--confirm-dialog-footer-padding-x: var(--spacing-padding-6);
|
|
1960
|
-
--confirm-dialog-footer-padding-y: var(--spacing-padding-7);
|
|
1961
|
-
--confirm-dialog-actions-gap: var(--spacing-gap-5);
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
.dialog-overlay {
|
|
1965
|
-
position: fixed;
|
|
1966
|
-
inset: 0;
|
|
1967
|
-
background-color: var(--dialog-overlay-bg);
|
|
1968
|
-
opacity: 0;
|
|
1969
|
-
transition: opacity 0.2s ease;
|
|
1970
|
-
will-change: opacity;
|
|
1971
|
-
}
|
|
1972
|
-
.dialog-overlay[data-state=open] {
|
|
1973
|
-
opacity: 1;
|
|
1974
|
-
}
|
|
1975
|
-
.dialog-overlay[data-state=closed] {
|
|
1976
|
-
opacity: 0;
|
|
1977
|
-
pointer-events: none;
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1981
|
-
.dialog-overlay {
|
|
1982
|
-
transition: none;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
.dialog-content {
|
|
1986
|
-
position: fixed;
|
|
1987
|
-
top: 50%;
|
|
1988
|
-
left: 50%;
|
|
1989
|
-
transform: translate(-50%, -50%);
|
|
1990
|
-
width: min(var(--dialog-panel-width), var(--dialog-panel-max-width));
|
|
1991
|
-
max-width: var(--dialog-panel-max-width);
|
|
1992
|
-
max-height: calc(100vh - var(--spacing-padding-10) * 2);
|
|
1993
|
-
background-color: var(--dialog-panel-bg);
|
|
1994
|
-
border-radius: var(--dialog-panel-radius);
|
|
1995
|
-
box-shadow: var(--dialog-panel-shadow);
|
|
1996
|
-
display: flex;
|
|
1997
|
-
flex-direction: column;
|
|
1998
|
-
overflow: hidden;
|
|
1999
|
-
overflow-y: auto;
|
|
2000
|
-
overscroll-behavior: contain;
|
|
2001
|
-
outline: none;
|
|
2002
|
-
border: none;
|
|
2003
|
-
gap: 0;
|
|
2004
|
-
opacity: 0;
|
|
2005
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
2006
|
-
}
|
|
2007
|
-
.dialog-content[data-state=open] {
|
|
2008
|
-
opacity: 1;
|
|
2009
|
-
transform: translate(-50%, -50%);
|
|
2010
|
-
}
|
|
2011
|
-
.dialog-content[data-state=closed] {
|
|
2012
|
-
opacity: 0;
|
|
2013
|
-
transform: translate(-50%, calc(-50% + 12px));
|
|
2014
|
-
pointer-events: none;
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
@media (prefers-reduced-motion: reduce) {
|
|
2018
|
-
.dialog-content {
|
|
2019
|
-
transition: none;
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
.dialog-section {
|
|
2023
|
-
padding: 0;
|
|
2024
|
-
}
|
|
2025
|
-
.dialog-section[data-section=actions] {
|
|
2026
|
-
flex-shrink: 0;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
.dialog-title {
|
|
2030
|
-
margin: 0;
|
|
2031
|
-
color: var(--dialog-title-color);
|
|
2032
|
-
font-size: var(--dialog-title-font-size);
|
|
2033
|
-
line-height: var(--dialog-title-line-height);
|
|
2034
|
-
font-weight: var(--dialog-title-weight);
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
.dialog-description {
|
|
2038
|
-
margin: 0;
|
|
2039
|
-
color: var(--dialog-description-color);
|
|
2040
|
-
font-size: var(--dialog-description-font-size);
|
|
2041
|
-
line-height: var(--dialog-description-line-height);
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
.dialog-button {
|
|
2045
|
-
display: flex;
|
|
2046
|
-
align-items: center;
|
|
2047
|
-
justify-content: center;
|
|
2048
|
-
width: 100%;
|
|
2049
|
-
border: none;
|
|
2050
|
-
border-radius: 0;
|
|
2051
|
-
margin: 0;
|
|
2052
|
-
padding: 0;
|
|
2053
|
-
font-size: var(--font-body-medium-size);
|
|
2054
|
-
line-height: var(--font-body-medium-line-height);
|
|
2055
|
-
font-weight: var(--font-body-medium-weight);
|
|
2056
|
-
cursor: pointer;
|
|
2057
|
-
background: transparent;
|
|
2058
|
-
color: var(--color-label-strong);
|
|
2059
|
-
transition: background-color 0.15s ease, color 0.15s ease;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
.dialog-button[data-native-element=true] {
|
|
2063
|
-
appearance: none;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
.notice-dialog-header,
|
|
2067
|
-
.notice-dialog-body {
|
|
2068
|
-
padding: var(--notice-dialog-section-padding-y) var(--notice-dialog-section-padding-x);
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
.notice-dialog-header {
|
|
2072
|
-
display: flex;
|
|
2073
|
-
flex-direction: column;
|
|
2074
|
-
gap: var(--spacing-gap-3);
|
|
2075
|
-
text-align: center;
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
.notice-dialog-body {
|
|
2079
|
-
text-align: center;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
.notice-dialog-body p {
|
|
2083
|
-
margin: 0 0 var(--spacing-gap-2);
|
|
2084
|
-
color: var(--dialog-body-color);
|
|
2085
|
-
font-size: var(--dialog-body-font-size);
|
|
2086
|
-
line-height: 1.5em;
|
|
2087
|
-
font-weight: var(--font-body-small-weight);
|
|
2088
|
-
word-break: keep-all;
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
.notice-dialog-body p:last-child {
|
|
2092
|
-
margin-bottom: 0;
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
.notice-dialog-actions {
|
|
2096
|
-
border-top: 1px solid var(--dialog-border-color);
|
|
2097
|
-
display: flex;
|
|
2098
|
-
align-items: stretch;
|
|
2099
|
-
flex-direction: row;
|
|
2100
|
-
padding: 0;
|
|
2101
|
-
gap: 0;
|
|
2102
|
-
justify-content: center;
|
|
2103
|
-
overflow: hidden;
|
|
2104
|
-
--button-min-height: var(--notice-dialog-action-height);
|
|
2105
|
-
--button-padding-inline: 0;
|
|
2106
|
-
--button-padding-block: 0;
|
|
2107
|
-
--button-border-radius: 0;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
.notice-dialog-actions > * {
|
|
2111
|
-
flex: 1;
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
.notice-dialog-actions .button {
|
|
2115
|
-
display: flex;
|
|
2116
|
-
align-items: center;
|
|
2117
|
-
justify-content: center;
|
|
2118
|
-
width: 100%;
|
|
2119
|
-
border: none;
|
|
2120
|
-
border-radius: 0;
|
|
2121
|
-
margin: 0;
|
|
2122
|
-
padding: 0;
|
|
2123
|
-
font-size: var(--font-body-medium-size);
|
|
2124
|
-
line-height: var(--font-body-medium-line-height);
|
|
2125
|
-
font-weight: var(--font-body-medium-weight);
|
|
2126
|
-
cursor: pointer;
|
|
2127
|
-
background: transparent;
|
|
2128
|
-
color: var(--color-label-strong);
|
|
2129
|
-
transition: background-color 0.15s ease, color 0.15s ease;
|
|
2130
|
-
background-color: var(--color-common-100);
|
|
2131
|
-
color: var(--color-primary-default);
|
|
2132
|
-
}
|
|
2133
|
-
.notice-dialog-actions .button:hover {
|
|
2134
|
-
background-color: var(--color-bg-surface-static-cool-gray);
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
.notice-dialog-actions .button:not(:first-child),
|
|
2138
|
-
.notice-dialog-actions [data-native-element=true]:not(:first-child) {
|
|
2139
|
-
border-left: 1px solid var(--dialog-border-color);
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
.confirm-dialog-header {
|
|
2143
|
-
padding: var(--confirm-dialog-header-padding-y) var(--confirm-dialog-header-padding-x);
|
|
2144
|
-
}
|
|
2145
|
-
|
|
2146
|
-
.confirm-dialog-body {
|
|
2147
|
-
padding: var(--confirm-dialog-body-padding-y) var(--confirm-dialog-body-padding-x);
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
.confirm-dialog-header {
|
|
2151
|
-
display: flex;
|
|
2152
|
-
flex-direction: column;
|
|
2153
|
-
gap: var(--spacing-gap-2);
|
|
2154
|
-
text-align: center;
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
.confirm-dialog-body {
|
|
2158
|
-
display: flex;
|
|
2159
|
-
flex-direction: column;
|
|
2160
|
-
align-items: center;
|
|
2161
|
-
gap: var(--spacing-gap-2);
|
|
2162
|
-
text-align: center;
|
|
2163
|
-
}
|
|
2164
|
-
|
|
2165
|
-
.confirm-dialog-body p {
|
|
2166
|
-
margin: 0;
|
|
2167
|
-
color: var(--dialog-body-color);
|
|
2168
|
-
font-size: var(--dialog-body-font-size);
|
|
2169
|
-
line-height: 1.5em;
|
|
2170
|
-
font-weight: var(--font-body-small-weight);
|
|
2171
|
-
word-break: keep-all;
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
.confirm-dialog-description {
|
|
2175
|
-
margin: 0;
|
|
2176
|
-
color: var(--dialog-body-color);
|
|
2177
|
-
font-size: var(--dialog-body-font-size);
|
|
2178
|
-
line-height: 1.5em;
|
|
2179
|
-
font-weight: var(--font-body-small-weight);
|
|
2180
|
-
word-break: keep-all;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
.confirm-dialog-actions {
|
|
2184
|
-
display: flex;
|
|
2185
|
-
align-items: stretch;
|
|
2186
|
-
flex-direction: row;
|
|
2187
|
-
padding: var(--confirm-dialog-footer-padding-y) var(--confirm-dialog-footer-padding-x);
|
|
2188
|
-
gap: var(--confirm-dialog-actions-gap);
|
|
2189
|
-
justify-content: center;
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
.confirm-dialog-actions > * {
|
|
2193
|
-
flex: 1;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
1935
|
:where(.radix-themes, .theme-root, :root) {
|
|
2197
1936
|
--drawer-overlay-bg: rgba(0, 0, 0, 0.44);
|
|
2198
1937
|
--drawer-surface-bg: var(--color-bg-surface-static-white);
|
|
@@ -2734,6 +2473,18 @@ figure.chip {
|
|
|
2734
2473
|
color: var(--theme-input-helper-color);
|
|
2735
2474
|
}
|
|
2736
2475
|
|
|
2476
|
+
.email-verification {
|
|
2477
|
+
display: flex;
|
|
2478
|
+
flex-direction: column;
|
|
2479
|
+
gap: var(--spacing-gap-4);
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
.email-verification__countdown {
|
|
2483
|
+
font-size: var(--font-caption-medium-size);
|
|
2484
|
+
line-height: var(--font-caption-medium-line-height);
|
|
2485
|
+
color: var(--theme-input-helper-color);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2737
2488
|
/* TODO(label): 스타일을 SOT 토큰 값으로 정의한다. */
|
|
2738
2489
|
:where(.radix-themes, .theme-root, :root) {
|
|
2739
2490
|
--theme-navigation-height: 86px;
|
|
@@ -2875,6 +2626,10 @@ figure.chip {
|
|
|
2875
2626
|
--pagination-dot-size: 8px;
|
|
2876
2627
|
--pagination-dot-bg: var(--color-cool-gray-85, #d2d3d7);
|
|
2877
2628
|
--pagination-dot-active-bg: var(--color-primary-default, #0061ff);
|
|
2629
|
+
--pagination-dot-active-bg-secondary: var(
|
|
2630
|
+
--color-bg-surface-heavy,
|
|
2631
|
+
#313235
|
|
2632
|
+
);
|
|
2878
2633
|
--pagination-carousel-height: 8px;
|
|
2879
2634
|
--pagination-carousel-dot-width: 8px;
|
|
2880
2635
|
--pagination-carousel-active-width: 20px;
|
|
@@ -2950,6 +2705,13 @@ figure.chip {
|
|
|
2950
2705
|
align-items: center;
|
|
2951
2706
|
}
|
|
2952
2707
|
|
|
2708
|
+
.pagination--variant-carousel[data-priority=secondary] {
|
|
2709
|
+
--pagination-dot-active-bg: var(
|
|
2710
|
+
--pagination-dot-active-bg-secondary,
|
|
2711
|
+
var(--color-secondary-strong, #ccdeff)
|
|
2712
|
+
);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2953
2715
|
.pagination--variant-carousel .pagination-button {
|
|
2954
2716
|
width: auto;
|
|
2955
2717
|
height: var(--pagination-carousel-height);
|
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
3
|
+
import type { InputProps, InputState } from "../../types";
|
|
4
|
+
import { Text } from "./Base";
|
|
5
|
+
import { IdentificationInput } from "./Identification";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EmailVerificationInput props. 이메일 입력 + 인증 요청/코드 입력 옵션을 정의한다.
|
|
9
|
+
* @property {string} [value] 제어형 값.
|
|
10
|
+
* @property {string} [defaultValue] 비제어 초기값.
|
|
11
|
+
* @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
|
|
12
|
+
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
|
|
13
|
+
* @property {() => void} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
|
|
14
|
+
* @property {string} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
|
|
15
|
+
* @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
|
|
16
|
+
* @property {ReactNode} [countdownText] 인증 제한 시간 안내 텍스트(optional).
|
|
17
|
+
* @property {boolean} [codeVisible] 인증번호 입력 UI 노출 여부(optional).
|
|
18
|
+
* @property {number} [codeLength=6] 인증번호 길이(optional).
|
|
19
|
+
* @property {ReactNode} [codeLabel] 인증번호 입력 label(optional).
|
|
20
|
+
* @property {ReactNode} [codeHelper] 인증번호 helper(optional).
|
|
21
|
+
* @property {InputState} [codeState] 인증번호 입력 상태(optional).
|
|
22
|
+
* @property {(code: string) => void} [onCodeComplete] 인증번호 입력 완료 시 호출(optional).
|
|
23
|
+
*/
|
|
24
|
+
export interface EmailVerificationInputProps extends Omit<
|
|
25
|
+
InputProps,
|
|
26
|
+
"type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
|
|
27
|
+
> {
|
|
28
|
+
value?: string;
|
|
29
|
+
defaultValue?: string;
|
|
30
|
+
onValueChange?: (value: string) => void;
|
|
31
|
+
onChange?: ComponentPropsWithoutRef<"input">["onChange"];
|
|
32
|
+
onRequestCode?: () => void;
|
|
33
|
+
requestButtonLabel?: string;
|
|
34
|
+
requestButtonDisabled?: boolean;
|
|
35
|
+
countdownText?: ReactNode;
|
|
36
|
+
codeVisible?: boolean;
|
|
37
|
+
codeLength?: number;
|
|
38
|
+
codeLabel?: ReactNode;
|
|
39
|
+
codeHelper?: ReactNode;
|
|
40
|
+
codeState?: InputState;
|
|
41
|
+
onCodeComplete?: (code: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 이메일 인증 입력 컴포넌트; 이메일 입력 + 인증요청 버튼 + OneTimeCode 입력을 옵션으로 제공한다.
|
|
46
|
+
* @component
|
|
47
|
+
* @param {EmailVerificationInputProps} props 이메일 인증 props
|
|
48
|
+
* @param {string} [props.value] 제어형 이메일 값
|
|
49
|
+
* @param {string} [props.defaultValue] 비제어 이메일 초기값
|
|
50
|
+
* @param {(value: string) => void} [props.onValueChange] 이메일 변경 콜백
|
|
51
|
+
* @param {ComponentPropsWithoutRef<"input">["onChange"]} [props.onChange] native onChange override
|
|
52
|
+
* @param {() => void} [props.onRequestCode] 인증요청 버튼 클릭 시 호출
|
|
53
|
+
* @param {string} [props.requestButtonLabel] 인증요청 버튼 라벨
|
|
54
|
+
* @param {boolean} [props.requestButtonDisabled] 인증요청 버튼 disabled
|
|
55
|
+
* @param {ReactNode} [props.countdownText] 제한 시간 안내 텍스트
|
|
56
|
+
* @param {boolean} [props.codeVisible] 인증번호 입력 UI 노출 여부
|
|
57
|
+
* @param {number} [props.codeLength] 인증번호 길이
|
|
58
|
+
* @param {ReactNode} [props.codeLabel] 인증번호 label
|
|
59
|
+
* @param {ReactNode} [props.codeHelper] 인증번호 helper
|
|
60
|
+
* @param {InputState} [props.codeState] 인증번호 입력 상태
|
|
61
|
+
* @param {(code: string) => void} [props.onCodeComplete] 인증번호 입력 완료 시 호출
|
|
62
|
+
*/
|
|
63
|
+
const EmailVerificationInput = forwardRef<
|
|
64
|
+
HTMLInputElement,
|
|
65
|
+
EmailVerificationInputProps
|
|
66
|
+
>(
|
|
67
|
+
(
|
|
68
|
+
{
|
|
69
|
+
value,
|
|
70
|
+
defaultValue,
|
|
71
|
+
onValueChange,
|
|
72
|
+
onChange,
|
|
73
|
+
onRequestCode,
|
|
74
|
+
requestButtonLabel = "인증번호 요청",
|
|
75
|
+
requestButtonDisabled,
|
|
76
|
+
countdownText,
|
|
77
|
+
codeVisible,
|
|
78
|
+
codeLength = 6,
|
|
79
|
+
codeLabel,
|
|
80
|
+
codeHelper,
|
|
81
|
+
codeState,
|
|
82
|
+
onCodeComplete,
|
|
83
|
+
right,
|
|
84
|
+
...restProps
|
|
85
|
+
},
|
|
86
|
+
forwardedRef,
|
|
87
|
+
) => {
|
|
88
|
+
const handleChange = useCallback(
|
|
89
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
90
|
+
onValueChange?.(event.currentTarget.value);
|
|
91
|
+
onChange?.(event);
|
|
92
|
+
},
|
|
93
|
+
[onChange, onValueChange],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const actionButton = useMemo(() => {
|
|
97
|
+
if (!onRequestCode) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className="input-action-button"
|
|
105
|
+
onClick={onRequestCode}
|
|
106
|
+
disabled={requestButtonDisabled}
|
|
107
|
+
>
|
|
108
|
+
{requestButtonLabel}
|
|
109
|
+
</button>
|
|
110
|
+
);
|
|
111
|
+
}, [onRequestCode, requestButtonDisabled, requestButtonLabel]);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="email-verification">
|
|
115
|
+
<Text
|
|
116
|
+
{...restProps}
|
|
117
|
+
ref={forwardedRef}
|
|
118
|
+
type="email"
|
|
119
|
+
inputMode="email"
|
|
120
|
+
value={value}
|
|
121
|
+
defaultValue={defaultValue}
|
|
122
|
+
onChange={handleChange}
|
|
123
|
+
right={right ?? actionButton}
|
|
124
|
+
/>
|
|
125
|
+
{countdownText ? (
|
|
126
|
+
<div className="email-verification__countdown">{countdownText}</div>
|
|
127
|
+
) : null}
|
|
128
|
+
{codeVisible ? (
|
|
129
|
+
<IdentificationInput
|
|
130
|
+
length={codeLength}
|
|
131
|
+
label={codeLabel}
|
|
132
|
+
helper={codeHelper}
|
|
133
|
+
state={codeState}
|
|
134
|
+
onComplete={onCodeComplete}
|
|
135
|
+
/>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
EmailVerificationInput.displayName = "EmailVerificationInput";
|
|
143
|
+
|
|
144
|
+
export { EmailVerificationInput };
|
|
@@ -10,9 +10,9 @@ import { Text } from "./Base";
|
|
|
10
10
|
* @property {string} [defaultValue] 비제어 초기값(숫자/포맷 모두 허용).
|
|
11
11
|
* @property {(value: string, digits: string) => void} [onValueChange] 포맷팅 값/숫자만 값을 함께 전달.
|
|
12
12
|
* @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
|
|
13
|
-
* @property {() => void} [onRequestCode]
|
|
14
|
-
* @property {string} [requestButtonLabel="인증번호 요청"]
|
|
15
|
-
* @property {boolean} [requestButtonDisabled]
|
|
13
|
+
* @property {() => void} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
|
|
14
|
+
* @property {string} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
|
|
15
|
+
* @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
|
|
16
16
|
*/
|
|
17
17
|
export interface PhoneInputProps extends Omit<
|
|
18
18
|
InputProps,
|
|
@@ -35,10 +35,16 @@ const normalizeDigits = (value?: string) => (value ?? "").replace(/\D/g, "");
|
|
|
35
35
|
const formatPhoneNumber = (digits: string) => maskPhone(digits);
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* 휴대폰 번호 입력 컴포넌트; 기본은 마스킹만 제공하고 인증 UI는 optional props로 노출한다.
|
|
39
39
|
* @component
|
|
40
|
-
* @param {PhoneInputProps} props
|
|
41
|
-
*
|
|
40
|
+
* @param {PhoneInputProps} props 휴대폰 입력 props
|
|
41
|
+
* @param {string} [props.value] 제어형 포맷 값
|
|
42
|
+
* @param {string} [props.defaultValue] 비제어 초기값
|
|
43
|
+
* @param {(value: string, digits: string) => void} [props.onValueChange] 포맷/숫자 변경 콜백
|
|
44
|
+
* @param {ComponentPropsWithoutRef<"input">["onChange"]} [props.onChange] native onChange override
|
|
45
|
+
* @param {() => void} [props.onRequestCode] 인증 요청 버튼 클릭 시 호출
|
|
46
|
+
* @param {string} [props.requestButtonLabel] 인증 요청 버튼 라벨
|
|
47
|
+
* @param {boolean} [props.requestButtonDisabled] 인증 요청 버튼 disabled
|
|
42
48
|
*/
|
|
43
49
|
const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
44
50
|
(
|
|
@@ -2,9 +2,11 @@ export { Text } from "./Base";
|
|
|
2
2
|
export { Text as Input } from "./Base";
|
|
3
3
|
export { PasswordInput } from "./Password";
|
|
4
4
|
export { PhoneInput } from "./Phone";
|
|
5
|
+
export { EmailVerificationInput } from "./EmailVerification";
|
|
5
6
|
export { SearchInput } from "./Search";
|
|
6
7
|
export { IdentificationInput } from "./Identification";
|
|
7
8
|
export type { InputPasswordProps } from "./Password";
|
|
8
9
|
export type { PhoneInputProps } from "./Phone";
|
|
10
|
+
export type { EmailVerificationInputProps } from "./EmailVerification";
|
|
9
11
|
export type { SearchInputProps } from "./Search";
|
|
10
12
|
export type { IdentificationInputProps } from "./Identification";
|
|
@@ -373,3 +373,15 @@
|
|
|
373
373
|
font-size: var(--font-label-small-size);
|
|
374
374
|
color: var(--theme-input-helper-color);
|
|
375
375
|
}
|
|
376
|
+
|
|
377
|
+
.email-verification {
|
|
378
|
+
display: flex;
|
|
379
|
+
flex-direction: column;
|
|
380
|
+
gap: var(--spacing-gap-4);
|
|
381
|
+
}
|
|
382
|
+
|
|
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);
|
|
387
|
+
}
|
|
@@ -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 {
|
package/src/index.scss
CHANGED
package/src/index.tsx
CHANGED
|
@@ -12,7 +12,6 @@ export * from "./components/radio";
|
|
|
12
12
|
export * from "./components/select";
|
|
13
13
|
export * from "./components/tab";
|
|
14
14
|
export * from "./components/navigation";
|
|
15
|
-
export * from "./components/dialog";
|
|
16
15
|
export * from "./components/dropdown";
|
|
17
16
|
export * from "./components/drawer";
|
|
18
17
|
export * from "./components/scrollbar";
|
package/src/types/form-field.ts
CHANGED
|
@@ -59,12 +59,12 @@ export interface InputFieldOptions {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Input Field
|
|
63
|
-
* @interface
|
|
62
|
+
* Input Field Props; attr + style + options + control props
|
|
63
|
+
* @interface InputFieldProps
|
|
64
64
|
* @template TProps
|
|
65
65
|
* @template FieldElement
|
|
66
66
|
*/
|
|
67
|
-
export interface
|
|
67
|
+
export interface InputFieldProps<
|
|
68
68
|
TProps extends InputProps = InputProps,
|
|
69
69
|
FieldElement extends HTMLElement = HTMLInputElement,
|
|
70
70
|
>
|