@uniai-fe/uds-primitives 0.1.13 → 0.2.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.
Files changed (114) hide show
  1. package/README.md +2 -2
  2. package/dist/styles.css +1112 -385
  3. package/package.json +12 -15
  4. package/src/components/button/index.scss +1 -0
  5. package/src/components/button/markup/{ButtonRounded.tsx → Rounded.tsx} +1 -1
  6. package/src/components/button/markup/{ButtonText.tsx → Text.tsx} +1 -1
  7. package/src/components/button/markup/index.ts +3 -3
  8. package/src/components/button/styles/button.scss +113 -229
  9. package/src/components/button/styles/round-button.scss +11 -14
  10. package/src/components/button/styles/text-button.scss +23 -23
  11. package/src/components/button/styles/variables.scss +145 -0
  12. package/src/components/dropdown/index.tsx +3 -3
  13. package/src/components/dropdown/markup/Template.tsx +57 -0
  14. package/src/components/dropdown/markup/foundation/Container.tsx +125 -0
  15. package/src/components/dropdown/markup/foundation/MenuItem.tsx +107 -0
  16. package/src/components/dropdown/markup/foundation/MenuList.tsx +27 -0
  17. package/src/components/dropdown/markup/foundation/Provider.tsx +46 -0
  18. package/src/components/dropdown/markup/foundation/Root.tsx +30 -0
  19. package/src/components/dropdown/markup/foundation/Trigger.tsx +34 -0
  20. package/src/components/dropdown/markup/foundation/index.tsx +25 -0
  21. package/src/components/dropdown/markup/index.tsx +8 -2
  22. package/src/components/dropdown/styles/dropdown.scss +166 -0
  23. package/src/components/dropdown/styles/index.scss +2 -0
  24. package/src/components/dropdown/styles/variables.scss +40 -0
  25. package/src/components/dropdown/types/base.ts +31 -0
  26. package/src/components/dropdown/types/index.ts +2 -4
  27. package/src/components/dropdown/types/props.ts +170 -0
  28. package/src/components/dropdown/utils/index.ts +1 -4
  29. package/src/components/dropdown/utils/refs.ts +20 -0
  30. package/src/components/form/index.scss +1 -0
  31. package/src/components/form/index.tsx +18 -2
  32. package/src/components/form/markup/form-field/Body.tsx +18 -0
  33. package/src/components/form/markup/form-field/Container.tsx +58 -0
  34. package/src/components/form/markup/form-field/Footer.tsx +21 -0
  35. package/src/components/form/markup/form-field/Header.tsx +39 -0
  36. package/src/components/form/markup/form-field/Template.tsx +56 -0
  37. package/src/components/form/markup/form-field/index.tsx +22 -0
  38. package/src/components/form/styles/form-field/layout.scss +67 -0
  39. package/src/components/form/styles/form-field/variables.scss +17 -0
  40. package/src/components/form/styles/index.scss +2 -0
  41. package/src/components/form/types/index.ts +1 -0
  42. package/src/components/form/types/props.ts +125 -0
  43. package/src/components/form/utils/form-field.ts +42 -0
  44. package/src/components/input/hooks/index.ts +1 -4
  45. package/src/components/input/hooks/useDigitField.ts +63 -0
  46. package/src/components/input/img/calendar/calendar.svg +7 -0
  47. package/src/components/input/img/calendar/chevron-down.svg +3 -0
  48. package/src/components/input/img/calendar/chevron-left.svg +3 -0
  49. package/src/components/input/img/calendar/chevron-right.svg +3 -0
  50. package/src/components/input/img/calendar/chevron-up.svg +3 -0
  51. package/src/components/input/index.tsx +2 -1
  52. package/src/components/input/markup/calendar/Base.tsx +329 -0
  53. package/src/components/input/markup/calendar/index.tsx +8 -0
  54. package/src/components/input/markup/{text/InputUtilityButton.tsx → foundation/Button.tsx} +5 -15
  55. package/src/components/input/markup/foundation/Input.tsx +245 -0
  56. package/src/components/input/markup/foundation/SideSlot.tsx +30 -0
  57. package/src/components/input/markup/foundation/StatusIcon.tsx +21 -0
  58. package/src/components/input/markup/foundation/Utility.tsx +103 -0
  59. package/src/components/input/markup/foundation/index.tsx +15 -0
  60. package/src/components/input/markup/index.tsx +11 -1
  61. package/src/components/input/markup/text/AuthCode.tsx +41 -59
  62. package/src/components/input/markup/text/Email.tsx +25 -115
  63. package/src/components/input/markup/text/Password.tsx +30 -39
  64. package/src/components/input/markup/text/Phone.tsx +35 -122
  65. package/src/components/input/markup/text/Search.tsx +17 -18
  66. package/src/components/input/markup/text/index.ts +15 -12
  67. package/src/components/input/styles/calendar.scss +110 -0
  68. package/src/components/input/styles/foundation.scss +345 -0
  69. package/src/components/input/styles/index.scss +4 -476
  70. package/src/components/input/styles/text.scss +89 -0
  71. package/src/components/input/styles/variables.scss +41 -0
  72. package/src/components/input/types/calendar.ts +208 -0
  73. package/src/components/input/types/foundation.ts +194 -0
  74. package/src/components/input/types/hooks.ts +43 -0
  75. package/src/components/input/types/index.ts +5 -87
  76. package/src/components/input/types/text.ts +203 -0
  77. package/src/components/input/types/verification.ts +23 -0
  78. package/src/components/input/utils/index.tsx +1 -0
  79. package/src/components/input/utils/verification.tsx +35 -0
  80. package/src/components/select/hooks/index.ts +43 -2
  81. package/src/components/select/img/chevron/primary/large.svg +3 -0
  82. package/src/components/select/img/chevron/primary/medium.svg +3 -0
  83. package/src/components/select/img/chevron/primary/small.svg +3 -0
  84. package/src/components/select/img/chevron/secondary/large.svg +3 -0
  85. package/src/components/select/img/chevron/secondary/medium.svg +3 -0
  86. package/src/components/select/img/chevron/secondary/small.svg +3 -0
  87. package/src/components/select/img/remove.svg +3 -0
  88. package/src/components/select/index.scss +2 -1
  89. package/src/components/select/index.tsx +5 -0
  90. package/src/components/select/markup/Default.tsx +154 -0
  91. package/src/components/select/markup/foundation/Base.tsx +90 -0
  92. package/src/components/select/markup/foundation/Container.tsx +30 -0
  93. package/src/components/select/markup/foundation/Icon.tsx +78 -0
  94. package/src/components/select/markup/foundation/Selected.tsx +34 -0
  95. package/src/components/select/markup/foundation/index.ts +2 -0
  96. package/src/components/select/markup/index.tsx +36 -2
  97. package/src/components/select/markup/multiple/Multiple.tsx +205 -0
  98. package/src/components/select/markup/multiple/SelectedChip.tsx +58 -0
  99. package/src/components/select/markup/multiple/index.ts +2 -0
  100. package/src/components/select/styles/select.scss +316 -0
  101. package/src/components/select/styles/variables.scss +91 -0
  102. package/src/components/select/types/base.ts +34 -0
  103. package/src/components/select/types/icon.ts +45 -0
  104. package/src/components/select/types/index.ts +6 -4
  105. package/src/components/select/types/multiple.ts +57 -0
  106. package/src/components/select/types/option.ts +43 -0
  107. package/src/components/select/types/props.ts +209 -0
  108. package/src/components/select/types/trigger.ts +196 -0
  109. package/src/index.scss +3 -2
  110. package/src/components/input/markup/text/Base.tsx +0 -454
  111. package/src/components/input/utils/index.ts +0 -60
  112. package/src/components/select/styles/index.scss +0 -0
  113. /package/src/components/button/markup/{ButtonDefault.tsx → Base.tsx} +0 -0
  114. /package/src/components/form/{Provider.tsx → markup/Provider.tsx} +0 -0
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+
5
+ import type { InputUtilityProps } from "../../types";
6
+ import { InputStatusIcon } from "./StatusIcon";
7
+ import InputBaseSideSlot from "./SideSlot";
8
+
9
+ /**
10
+ * Input; 오른쪽 유틸리티 영역(wrapper)
11
+ * @component
12
+ * @param {InputUtilProps} props
13
+ * @param {React.ReactNode} [props.right] 오른쪽 슬롯 콘텐츠
14
+ * @param {React.ReactNode} [props.clear] clear 버튼 아이콘
15
+ * @param {React.ReactNode} [props.success] success 상태 아이콘 override
16
+ * @param {React.ReactNode} [props.error] error 상태 아이콘 override
17
+ * @param {string} props.state 현재 input 상태
18
+ * @param {boolean} props.isDisabled disable 여부
19
+ * @param {boolean} props.isFocused focus 여부
20
+ * @param {boolean} props.hasValue 입력값 존재 여부
21
+ * @param {boolean} [props.readOnly] readOnly 여부
22
+ * @param {Function} [props.onClear] clear 버튼 클릭 핸들러
23
+ */
24
+ export default function InputBaseUtil({
25
+ children: right,
26
+ clear,
27
+ success,
28
+ error,
29
+ state,
30
+ isDisabled,
31
+ isFocused,
32
+ hasValue,
33
+ readOnly,
34
+ onClear,
35
+ }: InputUtilityProps) {
36
+ const [isClearInteracting, setIsClearInteracting] = useState(false);
37
+
38
+ const baseStatusIcon = InputStatusIcon[state] ?? null;
39
+ const statusIcon =
40
+ state === "success"
41
+ ? (success ?? baseStatusIcon)
42
+ : state === "error"
43
+ ? (error ?? baseStatusIcon)
44
+ : null;
45
+ const clearIconNode = clear ?? InputStatusIcon.reset;
46
+ const showClearIcon = Boolean(
47
+ clearIconNode &&
48
+ hasValue &&
49
+ !(isDisabled || readOnly) &&
50
+ (isFocused || isClearInteracting),
51
+ );
52
+
53
+ if (!right && !showClearIcon && !statusIcon) {
54
+ return null;
55
+ }
56
+
57
+ const handleClearPointerDown = () => {
58
+ setIsClearInteracting(true);
59
+ };
60
+
61
+ const handleClearPointerLeave = () => {
62
+ setIsClearInteracting(false);
63
+ };
64
+
65
+ const handleClearPointerUp = (
66
+ event: React.PointerEvent<HTMLButtonElement>,
67
+ ) => {
68
+ setIsClearInteracting(false);
69
+ onClear?.(event);
70
+ };
71
+
72
+ return (
73
+ // 유틸리티 영역; 오른쪽 슬롯/clear/status 정렬
74
+ <div className="input-field-utilities">
75
+ {right && <InputBaseSideSlot type="right">{right}</InputBaseSideSlot>}
76
+ {showClearIcon ? (
77
+ <button
78
+ type="button"
79
+ className="input-affix input-affix-clear"
80
+ data-slot="clear"
81
+ data-visible="true"
82
+ onClick={onClear}
83
+ onPointerDown={handleClearPointerDown}
84
+ onPointerLeave={handleClearPointerLeave}
85
+ onPointerUp={handleClearPointerUp}
86
+ onPointerCancel={handleClearPointerLeave}
87
+ aria-label="입력 내용 지우기"
88
+ >
89
+ {clearIconNode}
90
+ </button>
91
+ ) : null}
92
+ {statusIcon ? (
93
+ <div
94
+ className="input-affix input-affix-status"
95
+ data-slot="status"
96
+ data-state={state}
97
+ >
98
+ {statusIcon}
99
+ </div>
100
+ ) : null}
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,15 @@
1
+ "use client";
2
+
3
+ import InputBase from "./Input";
4
+ import InputBaseSideSlot from "./SideSlot";
5
+ import InputBaseUtil from "./Utility";
6
+ import InputBaseUtilityButton from "./Button";
7
+ import { InputStatusIcon } from "./StatusIcon";
8
+
9
+ export const InputFoundation = {
10
+ Base: InputBase,
11
+ Util: InputBaseUtil,
12
+ SideSlot: InputBaseSideSlot,
13
+ Icon: InputStatusIcon,
14
+ Button: InputBaseUtilityButton,
15
+ };
@@ -1 +1,11 @@
1
- export * from "./text";
1
+ "use client";
2
+
3
+ import { InputFoundation } from "./foundation";
4
+ import { InputText } from "./text";
5
+ import { InputCalendar } from "./calendar";
6
+
7
+ export const Input = {
8
+ ...InputFoundation,
9
+ Text: InputText,
10
+ Calendar: InputCalendar,
11
+ };
@@ -1,51 +1,41 @@
1
- import type { ChangeEvent, ComponentPropsWithoutRef, ReactNode } from "react";
2
- import { forwardRef, useCallback, useMemo, useState } from "react";
3
- import { Text } from "./Base";
4
- import { InputUtilityButton } from "./InputUtilityButton";
5
- import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
6
- import type { InputProps } from "../../types";
1
+ "use client";
7
2
 
8
- const normalizeDigits = (value?: string) => (value ?? "").replace(/\D/g, "");
9
- const clampLength = (length?: number) => Math.max(4, Math.min(8, length ?? 6));
3
+ import type { ChangeEvent } from "react";
4
+ import { forwardRef, useCallback, useMemo } from "react";
5
+ import InputBase from "../foundation/Input";
6
+ import InputBaseUtilityButton from "../foundation/Button";
7
+ import { useDigitField } from "../../hooks/useDigitField";
8
+ import type { AuthCodeInputProps } from "../../types";
10
9
 
11
10
  /**
12
- * AuthCodeInput props. 이메일/휴대폰 컴포넌트와 동일한 UX로 인증코드를 입력한다.
13
- * @property {string} [value] 제어형 값.
14
- * @property {string} [defaultValue] 비제어 초기값.
15
- * @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
16
- * @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
17
- * @property {number} [length=6] 허용 자리수(4~8 사이로 보정).
18
- * @property {(code: string) => void} [onComplete] length만큼 입력되면 호출.
19
- * @property {string} [placeholder="인증코드 입력"] placeholder 텍스트.
20
- * @property {ReactNode} [countdownText] 제한 시간 텍스트.
21
- * @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨.
22
- * @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 핸들러.
23
- * @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled.
11
+ * 인증코드 길이를 4~8자 범위로 보정한다.
24
12
  */
25
- export interface AuthCodeInputProps extends Omit<
26
- InputProps,
27
- "type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
28
- > {
29
- value?: string;
30
- defaultValue?: string;
31
- onValueChange?: (value: string) => void;
32
- onChange?: ComponentPropsWithoutRef<"input">["onChange"];
33
- length?: number;
34
- onComplete?: (code: string) => void;
35
- placeholder?: string;
36
- countdownText?: ReactNode;
37
- countdownActionLabel?: ReactNode;
38
- onCountdownAction?: InputUtilityButtonClickHandler;
39
- countdownActionDisabled?: boolean;
40
- }
13
+ const clampLength = (length?: number) => Math.max(4, Math.min(8, length ?? 6));
41
14
 
15
+ /**
16
+ * AuthCode 입력 필드 기본 placeholder.
17
+ */
42
18
  const DEFAULT_PLACEHOLDER = "인증코드 입력";
19
+ /**
20
+ * 제한 시간 연장 버튼 기본 라벨.
21
+ */
43
22
  const DEFAULT_COUNTDOWN_ACTION_LABEL = "시간연장";
44
23
 
45
24
  /**
46
25
  * AuthCodeInput — Text Input priority secondary 스타일로 구성된 인증번호 입력.
47
26
  * @component
48
27
  * @param {AuthCodeInputProps} props 인증코드 입력 props
28
+ * @property {string} [props.value] 제어형 인증코드 값
29
+ * @property {string} [props.defaultValue] 비제어 초기값
30
+ * @property {(value: string) => void} [props.onValueChange] 값 변경 콜백
31
+ * @property {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override
32
+ * @property {number} [props.length=6] 허용 자리수(4~8로 보정)
33
+ * @property {(code: string) => void} [props.onComplete] 자리수 충족 시 호출
34
+ * @property {string} [props.placeholder="인증코드 입력"] placeholder 텍스트
35
+ * @property {ReactNode} [props.countdownText] 제한 시간 표시
36
+ * @property {ReactNode} [props.countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨
37
+ * @property {InputUtilityButtonClickHandler} [props.onCountdownAction] 연장 버튼 클릭 핸들러
38
+ * @property {boolean} [props.countdownActionDisabled] 연장 버튼 disabled 여부
49
39
  */
50
40
  const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
51
41
  (
@@ -68,31 +58,22 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
68
58
  forwardedRef,
69
59
  ) => {
70
60
  const safeLength = clampLength(length);
71
- const [innerValue, setInnerValue] = useState(() =>
72
- normalizeDigits(defaultValue),
73
- );
74
- const isControlled = value !== undefined;
75
- const resolvedValue = isControlled ? normalizeDigits(value) : innerValue;
61
+ const { digits: resolvedValue, handleDigitsChange } = useDigitField({
62
+ value,
63
+ defaultValue,
64
+ maxLength: safeLength,
65
+ });
76
66
 
77
67
  const handleChange = useCallback(
78
68
  (event: ChangeEvent<HTMLInputElement>) => {
79
- const digits = normalizeDigits(event.currentTarget.value).slice(
80
- 0,
81
- safeLength,
82
- );
83
- if (!isControlled) {
84
- setInnerValue(digits);
85
- }
86
- if (event.currentTarget.value !== digits) {
69
+ const digits = handleDigitsChange(event);
70
+ if (event.currentTarget.value !== digits)
87
71
  event.currentTarget.value = digits;
88
- }
89
72
  onValueChange?.(digits);
90
- if (digits.length === safeLength) {
91
- onComplete?.(digits);
92
- }
73
+ if (digits.length === safeLength) onComplete?.(digits);
93
74
  onChange?.(event);
94
75
  },
95
- [isControlled, onChange, onComplete, onValueChange, safeLength],
76
+ [handleDigitsChange, onChange, onComplete, onValueChange, safeLength],
96
77
  );
97
78
 
98
79
  const countdownActions = useMemo(() => {
@@ -101,18 +82,19 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
101
82
  }
102
83
 
103
84
  return (
104
- <div className="auth-code-input__actions">
85
+ // 인증 타이머/액션 영역
86
+ <div className="auth-code-input-actions">
105
87
  {countdownText ? (
106
- <span className="auth-code-input__countdown">{countdownText}</span>
88
+ <span className="auth-code-input-countdown">{countdownText}</span>
107
89
  ) : null}
108
90
  {onCountdownAction ? (
109
- <InputUtilityButton
91
+ <InputBaseUtilityButton
110
92
  priority="tertiary"
111
93
  onClick={onCountdownAction}
112
94
  disabled={countdownActionDisabled}
113
95
  >
114
96
  {countdownActionLabel}
115
- </InputUtilityButton>
97
+ </InputBaseUtilityButton>
116
98
  ) : null}
117
99
  </div>
118
100
  );
@@ -124,7 +106,7 @@ const AuthCodeInput = forwardRef<HTMLInputElement, AuthCodeInputProps>(
124
106
  ]);
125
107
 
126
108
  return (
127
- <Text
109
+ <InputBase
128
110
  {...restProps}
129
111
  ref={forwardedRef}
130
112
  priority={priority}
@@ -1,77 +1,24 @@
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 { AuthCodeInput } from "./AuthCode";
6
- import type { AuthCodeInputProps } from "./AuthCode";
7
- import { InputUtilityButton } from "./InputUtilityButton";
8
- import type { InputUtilityButtonClickHandler } from "./InputUtilityButton";
1
+ "use client";
9
2
 
10
- /**
11
- * EmailInput props. 이메일 입력 + 인증 요청/코드 입력 옵션을 정의한다.
12
- * @property {string} [value] 제어형 값.
13
- * @property {string} [defaultValue] 비제어 초기값.
14
- * @property {(value: string) => void} [onValueChange] 값 변경 시 호출.
15
- * @property {ComponentPropsWithoutRef<"input">["onChange"]} [onChange] native onChange override.
16
- * @property {InputUtilityButtonClickHandler} [onRequestCode] 인증 요청 버튼 클릭 시 호출(optional).
17
- * @property {ReactNode} [requestButtonLabel="인증번호 요청"] 인증 요청 버튼 라벨(optional).
18
- * @property {boolean} [requestButtonDisabled] 인증 요청 버튼 disabled(optional).
19
- * @property {ReactNode} [countdownText] 인증 제한 시간 안내 텍스트(optional).
20
- * @property {ReactNode} [countdownActionLabel="시간연장"] 제한 시간 연장 버튼 라벨(optional).
21
- * @property {InputUtilityButtonClickHandler} [onCountdownAction] 제한 시간 연장 버튼 클릭 콜백(optional).
22
- * @property {boolean} [countdownActionDisabled] 제한 시간 연장 버튼 disabled(optional).
23
- * @property {boolean} [codeVisible] 인증번호 입력 UI 노출 여부(optional).
24
- * @property {number} [codeLength=6] 인증번호 길이(optional).
25
- * @property {ReactNode} [codeLabel] 인증번호 입력 label(optional).
26
- * @property {ReactNode} [codeHelper] 인증번호 helper(optional).
27
- * @property {InputState} [codeState] 인증번호 입력 상태(optional).
28
- * @property {(code: string) => void} [onCodeComplete] 인증번호 입력 완료 시 호출(optional).
29
- * @property {string} [codePlaceholder] 인증번호 입력 placeholder(optional).
30
- * @property {Partial<AuthCodeInputProps>} [codeInputProps] 인증코드 입력 추가 설정(register 등) 전달용.
31
- */
32
- export interface EmailInputProps extends Omit<
33
- InputProps,
34
- "type" | "inputMode" | "pattern" | "onChange" | "value" | "defaultValue"
35
- > {
36
- value?: string;
37
- defaultValue?: string;
38
- onValueChange?: (value: string) => void;
39
- onChange?: ComponentPropsWithoutRef<"input">["onChange"];
40
- onRequestCode?: InputUtilityButtonClickHandler;
41
- requestButtonLabel?: ReactNode;
42
- requestButtonDisabled?: boolean;
43
- countdownText?: ReactNode;
44
- countdownActionLabel?: ReactNode;
45
- onCountdownAction?: InputUtilityButtonClickHandler;
46
- countdownActionDisabled?: boolean;
47
- codeVisible?: boolean;
48
- codeLength?: number;
49
- codeLabel?: ReactNode;
50
- codeHelper?: ReactNode;
51
- codeState?: InputState;
52
- onCodeComplete?: (code: string) => void;
53
- codePlaceholder?: string;
54
- codeInputProps?: Partial<AuthCodeInputProps>;
55
- }
3
+ import type { ChangeEvent } from "react";
4
+ import { forwardRef, useCallback } from "react";
5
+
6
+ import type { EmailInputProps } from "../../types";
7
+ import InputBase from "../foundation/Input";
8
+ import { renderVerificationRequestButton } from "../../utils/verification";
56
9
 
57
10
  /**
58
- * 이메일 인증 입력 컴포넌트; 이메일 입력 + 인증요청 버튼 + OneTimeCode 입력을 옵션으로 제공한다.
11
+ * EmailInput 이메일 입력과 인증요청 버튼만 제공한다.
59
12
  * @component
60
- * @param {EmailInputProps} props 이메일 인증 props
61
- * @param {string} [props.value] 제어형 이메일 값
62
- * @param {string} [props.defaultValue] 비제어 이메일 초기값
63
- * @param {(value: string) => void} [props.onValueChange] 이메일 변경 콜백
64
- * @param {ComponentPropsWithoutRef<"input">["onChange"]} [props.onChange] native onChange override
65
- * @param {InputUtilityButtonClickHandler} [props.onRequestCode] 인증요청 버튼 클릭 시 호출
66
- * @param {string} [props.requestButtonLabel] 인증요청 버튼 라벨
67
- * @param {boolean} [props.requestButtonDisabled] 인증요청 버튼 disabled
68
- * @param {ReactNode} [props.countdownText] 제한 시간 안내 텍스트
69
- * @param {boolean} [props.codeVisible] 인증번호 입력 UI 노출 여부
70
- * @param {number} [props.codeLength] 인증번호 길이
71
- * @param {ReactNode} [props.codeLabel] 인증번호 label
72
- * @param {ReactNode} [props.codeHelper] 인증번호 helper
73
- * @param {InputState} [props.codeState] 인증번호 입력 상태
74
- * @param {(code: string) => void} [props.onCodeComplete] 인증번호 입력 완료 시 호출
13
+ * @param {EmailInputProps} props 이메일 입력 props
14
+ * @property {string} [props.value] 제어형 이메일 값
15
+ * @property {string} [props.defaultValue] 비제어 초기값
16
+ * @property {(value: string) => void} [props.onValueChange] 변경 콜백
17
+ * @property {(event: ChangeEvent<HTMLInputElement>) => void} [props.onChange] native onChange override
18
+ * @property {InputUtilityButtonClickHandler} [props.onRequestCode] 인증요청 버튼 핸들러
19
+ * @property {ReactNode} [props.requestButtonLabel] 버튼 라벨
20
+ * @property {boolean} [props.requestButtonDisabled] 버튼 disabled 상태
21
+ * @property {ReactNode} [props.right] 기본 오른쪽 슬롯 override
75
22
  */
76
23
  const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
77
24
  (
@@ -83,23 +30,12 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
83
30
  onRequestCode,
84
31
  requestButtonLabel = "인증번호 요청",
85
32
  requestButtonDisabled,
86
- countdownText,
87
- countdownActionLabel = "시간연장",
88
- onCountdownAction,
89
- countdownActionDisabled,
90
- codeVisible,
91
- codeLength = 6,
92
- codeLabel,
93
- codeHelper,
94
- codeState,
95
- onCodeComplete,
96
- codePlaceholder = "인증코드 입력",
97
33
  right,
98
- codeInputProps,
99
34
  ...restProps
100
35
  },
101
36
  forwardedRef,
102
37
  ) => {
38
+ // 이메일 값 변경 시 상위 상태(onValueChange)와 native onChange를 모두 호출한다.
103
39
  const handleChange = useCallback(
104
40
  (event: ChangeEvent<HTMLInputElement>) => {
105
41
  onValueChange?.(event.currentTarget.value);
@@ -108,41 +44,16 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
108
44
  [onChange, onValueChange],
109
45
  );
110
46
 
111
- const actionButton = useMemo(() => {
112
- if (!onRequestCode) {
113
- return null;
114
- }
115
-
116
- return (
117
- <InputUtilityButton
118
- priority="primary"
119
- onClick={onRequestCode}
120
- disabled={requestButtonDisabled}
121
- >
122
- {requestButtonLabel}
123
- </InputUtilityButton>
124
- );
125
- }, [onRequestCode, requestButtonDisabled, requestButtonLabel]);
126
-
127
- const resolvedCodeProps: AuthCodeInputProps = {
128
- ...(codeInputProps ?? {}),
129
- length: codeLength ?? codeInputProps?.length,
130
- label: codeLabel ?? codeInputProps?.label,
131
- helper: codeHelper ?? codeInputProps?.helper,
132
- state: codeState ?? codeInputProps?.state,
133
- placeholder: codePlaceholder ?? codeInputProps?.placeholder,
134
- countdownText: countdownText ?? codeInputProps?.countdownText,
135
- countdownActionLabel:
136
- countdownActionLabel ?? codeInputProps?.countdownActionLabel,
137
- onCountdownAction: onCountdownAction ?? codeInputProps?.onCountdownAction,
138
- countdownActionDisabled:
139
- countdownActionDisabled ?? codeInputProps?.countdownActionDisabled,
140
- onComplete: onCodeComplete ?? codeInputProps?.onComplete,
141
- };
47
+ // 인증 요청 옵션이 있을 때만 우측 액션 버튼을 만든다.
48
+ const actionButton = renderVerificationRequestButton({
49
+ onRequestCode,
50
+ requestButtonDisabled,
51
+ requestButtonLabel,
52
+ });
142
53
 
143
54
  return (
144
55
  <div className="email-verification">
145
- <Text
56
+ <InputBase
146
57
  {...restProps}
147
58
  ref={forwardedRef}
148
59
  type="email"
@@ -152,7 +63,6 @@ const EmailInput = forwardRef<HTMLInputElement, EmailInputProps>(
152
63
  onChange={handleChange}
153
64
  right={right ?? actionButton}
154
65
  />
155
- {codeVisible ? <AuthCodeInput {...resolvedCodeProps} /> : null}
156
66
  </div>
157
67
  );
158
68
  },
@@ -1,29 +1,19 @@
1
- import { forwardRef, useMemo, useState } from "react";
2
- import type { InputProps } from "../../types";
3
- import { Text } from "./Base";
1
+ "use client";
2
+
3
+ import { forwardRef, useCallback, useState } from "react";
4
+ import type { InputPasswordProps } from "../../types";
5
+ import InputBase from "../foundation/Input";
4
6
  import HideOffIcon from "../../img/hide-off.svg";
5
7
  import HideOnIcon from "../../img/hide-on.svg";
6
8
 
7
- /**
8
- * PasswordInput 전용 props. Text Input props에서 type을 password 전용으로 고정하고 보기/숨김 토글 옵션을 확장한다.
9
- * @property {boolean} [defaultVisible=false] 초기 렌더 시 비밀번호를 드러낼지 여부.
10
- * @property {{show: string; hide: string}} [toggleLabel] 토글 버튼에 사용할 라벨 텍스트 집합.
11
- */
12
- export interface InputPasswordProps extends Omit<InputProps, "type"> {
13
- defaultVisible?: boolean;
14
- toggleLabel?: {
15
- show: string;
16
- hide: string;
17
- };
18
- }
19
-
20
9
  /**
21
10
  * PasswordInput — 기본 Text 입력을 비밀번호 토글 UX로 확장한다.
22
11
  * @component
23
12
  * @param {InputPasswordProps} props
24
- * @param {boolean} [props.defaultVisible=false] 초기 노출 여부.
25
- * @param {{show: string; hide: string}} [props.toggleLabel={show:"보기",hide:"숨김"}] 토글 버튼 라벨.
26
- * 나머지 props Text Input과 동일하다.
13
+ * @property {boolean} [props.defaultVisible=false] 초기 노출 여부
14
+ * @property {{show: string; hide: string}} [props.toggleLabel] 토글 버튼 라벨
15
+ * @property {ReactNode} [props.right] 오른쪽 슬롯 override
16
+ * @property {string | number | readonly string[]} [props.defaultValue] 초기값
27
17
  */
28
18
  const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
29
19
  (
@@ -38,33 +28,34 @@ const PasswordInput = forwardRef<HTMLInputElement, InputPasswordProps>(
38
28
  ) => {
39
29
  const [visible, setVisible] = useState(defaultVisible);
40
30
 
41
- const toggleButton = useMemo(
42
- () => (
43
- <button
44
- type="button"
45
- className="input-password-toggle"
46
- onClick={() => setVisible(prev => !prev)}
47
- aria-pressed={visible}
48
- aria-label={visible ? toggleLabel.hide : toggleLabel.show}
49
- >
50
- {/* 토글 상태에 따라 hide-on/off 아이콘을 교체한다. */}
51
- {visible ? (
52
- <HideOffIcon aria-hidden="true" />
53
- ) : (
54
- <HideOnIcon aria-hidden="true" />
55
- )}
56
- </button>
57
- ),
58
- [toggleLabel.hide, toggleLabel.show, visible],
31
+ const handleToggle = useCallback(() => {
32
+ setVisible(prev => !prev);
33
+ }, []);
34
+
35
+ const toggleButton = (
36
+ <button
37
+ type="button"
38
+ className="input-password-toggle"
39
+ onClick={handleToggle}
40
+ aria-pressed={visible}
41
+ aria-label={visible ? toggleLabel.hide : toggleLabel.show}
42
+ >
43
+ {/* 토글 상태에 따라 hide-on/off 아이콘을 교체한다. */}
44
+ {visible ? (
45
+ <HideOffIcon aria-hidden="true" />
46
+ ) : (
47
+ <HideOnIcon aria-hidden="true" />
48
+ )}
49
+ </button>
59
50
  );
60
51
 
61
52
  return (
62
- <Text
63
- {...restProps}
53
+ <InputBase
64
54
  ref={forwardedRef}
65
55
  type={visible ? "text" : "password"}
66
56
  defaultValue={defaultValue}
67
57
  right={right ?? toggleButton}
58
+ {...restProps}
68
59
  />
69
60
  );
70
61
  },