@space-uy/pulsar-ui 0.2.0

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 (155) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +148 -0
  3. package/lib/module/components/Accordion.js +242 -0
  4. package/lib/module/components/Accordion.js.map +1 -0
  5. package/lib/module/components/BottomSheet.js +183 -0
  6. package/lib/module/components/BottomSheet.js.map +1 -0
  7. package/lib/module/components/Button.js +64 -0
  8. package/lib/module/components/Button.js.map +1 -0
  9. package/lib/module/components/ButtonContainer.js +118 -0
  10. package/lib/module/components/ButtonContainer.js.map +1 -0
  11. package/lib/module/components/CalendarPicker.js +374 -0
  12. package/lib/module/components/CalendarPicker.js.map +1 -0
  13. package/lib/module/components/Card.js +43 -0
  14. package/lib/module/components/Card.js.map +1 -0
  15. package/lib/module/components/Checkbox.js +122 -0
  16. package/lib/module/components/Checkbox.js.map +1 -0
  17. package/lib/module/components/Chip.js +50 -0
  18. package/lib/module/components/Chip.js.map +1 -0
  19. package/lib/module/components/CopyToClipboard.js +98 -0
  20. package/lib/module/components/CopyToClipboard.js.map +1 -0
  21. package/lib/module/components/Dialog.js +232 -0
  22. package/lib/module/components/Dialog.js.map +1 -0
  23. package/lib/module/components/Header.js +94 -0
  24. package/lib/module/components/Header.js.map +1 -0
  25. package/lib/module/components/Icon.js +22 -0
  26. package/lib/module/components/Icon.js.map +1 -0
  27. package/lib/module/components/IconButton.js +57 -0
  28. package/lib/module/components/IconButton.js.map +1 -0
  29. package/lib/module/components/Input.js +111 -0
  30. package/lib/module/components/Input.js.map +1 -0
  31. package/lib/module/components/InputContainer.js +104 -0
  32. package/lib/module/components/InputContainer.js.map +1 -0
  33. package/lib/module/components/LoadingIndicator.js +62 -0
  34. package/lib/module/components/LoadingIndicator.js.map +1 -0
  35. package/lib/module/components/OtpInput.js +85 -0
  36. package/lib/module/components/OtpInput.js.map +1 -0
  37. package/lib/module/components/OtpInputContainer.js +148 -0
  38. package/lib/module/components/OtpInputContainer.js.map +1 -0
  39. package/lib/module/components/Select.js +189 -0
  40. package/lib/module/components/Select.js.map +1 -0
  41. package/lib/module/components/Switch.js +74 -0
  42. package/lib/module/components/Switch.js.map +1 -0
  43. package/lib/module/components/Tabs.js +99 -0
  44. package/lib/module/components/Tabs.js.map +1 -0
  45. package/lib/module/components/Text.js +66 -0
  46. package/lib/module/components/Text.js.map +1 -0
  47. package/lib/module/components/TextArea.js +106 -0
  48. package/lib/module/components/TextArea.js.map +1 -0
  49. package/lib/module/hooks/useTheme.js +20 -0
  50. package/lib/module/hooks/useTheme.js.map +1 -0
  51. package/lib/module/index.js +27 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/module/package.json +1 -0
  54. package/lib/module/store/themeStore.js +50 -0
  55. package/lib/module/store/themeStore.js.map +1 -0
  56. package/lib/module/theme/colors.js +25 -0
  57. package/lib/module/theme/colors.js.map +1 -0
  58. package/lib/module/theme/meassures.js +10 -0
  59. package/lib/module/theme/meassures.js.map +1 -0
  60. package/lib/module/utils/stringUtils.js +12 -0
  61. package/lib/module/utils/stringUtils.js.map +1 -0
  62. package/lib/module/utils/uiUtils.js +63 -0
  63. package/lib/module/utils/uiUtils.js.map +1 -0
  64. package/lib/typescript/package.json +1 -0
  65. package/lib/typescript/src/components/Accordion.d.ts +22 -0
  66. package/lib/typescript/src/components/Accordion.d.ts.map +1 -0
  67. package/lib/typescript/src/components/BottomSheet.d.ts +13 -0
  68. package/lib/typescript/src/components/BottomSheet.d.ts.map +1 -0
  69. package/lib/typescript/src/components/Button.d.ts +16 -0
  70. package/lib/typescript/src/components/Button.d.ts.map +1 -0
  71. package/lib/typescript/src/components/ButtonContainer.d.ts +30 -0
  72. package/lib/typescript/src/components/ButtonContainer.d.ts.map +1 -0
  73. package/lib/typescript/src/components/CalendarPicker.d.ts +19 -0
  74. package/lib/typescript/src/components/CalendarPicker.d.ts.map +1 -0
  75. package/lib/typescript/src/components/Card.d.ts +7 -0
  76. package/lib/typescript/src/components/Card.d.ts.map +1 -0
  77. package/lib/typescript/src/components/Checkbox.d.ts +11 -0
  78. package/lib/typescript/src/components/Checkbox.d.ts.map +1 -0
  79. package/lib/typescript/src/components/Chip.d.ts +9 -0
  80. package/lib/typescript/src/components/Chip.d.ts.map +1 -0
  81. package/lib/typescript/src/components/CopyToClipboard.d.ts +12 -0
  82. package/lib/typescript/src/components/CopyToClipboard.d.ts.map +1 -0
  83. package/lib/typescript/src/components/Dialog.d.ts +40 -0
  84. package/lib/typescript/src/components/Dialog.d.ts.map +1 -0
  85. package/lib/typescript/src/components/Header.d.ts +18 -0
  86. package/lib/typescript/src/components/Header.d.ts.map +1 -0
  87. package/lib/typescript/src/components/Icon.d.ts +12 -0
  88. package/lib/typescript/src/components/Icon.d.ts.map +1 -0
  89. package/lib/typescript/src/components/IconButton.d.ts +13 -0
  90. package/lib/typescript/src/components/IconButton.d.ts.map +1 -0
  91. package/lib/typescript/src/components/Input.d.ts +17 -0
  92. package/lib/typescript/src/components/Input.d.ts.map +1 -0
  93. package/lib/typescript/src/components/InputContainer.d.ts +22 -0
  94. package/lib/typescript/src/components/InputContainer.d.ts.map +1 -0
  95. package/lib/typescript/src/components/LoadingIndicator.d.ts +9 -0
  96. package/lib/typescript/src/components/LoadingIndicator.d.ts.map +1 -0
  97. package/lib/typescript/src/components/OtpInput.d.ts +3 -0
  98. package/lib/typescript/src/components/OtpInput.d.ts.map +1 -0
  99. package/lib/typescript/src/components/OtpInputContainer.d.ts +17 -0
  100. package/lib/typescript/src/components/OtpInputContainer.d.ts.map +1 -0
  101. package/lib/typescript/src/components/Select.d.ts +20 -0
  102. package/lib/typescript/src/components/Select.d.ts.map +1 -0
  103. package/lib/typescript/src/components/Switch.d.ts +10 -0
  104. package/lib/typescript/src/components/Switch.d.ts.map +1 -0
  105. package/lib/typescript/src/components/Tabs.d.ts +14 -0
  106. package/lib/typescript/src/components/Tabs.d.ts.map +1 -0
  107. package/lib/typescript/src/components/Text.d.ts +7 -0
  108. package/lib/typescript/src/components/Text.d.ts.map +1 -0
  109. package/lib/typescript/src/components/TextArea.d.ts +16 -0
  110. package/lib/typescript/src/components/TextArea.d.ts.map +1 -0
  111. package/lib/typescript/src/hooks/useTheme.d.ts +9 -0
  112. package/lib/typescript/src/hooks/useTheme.d.ts.map +1 -0
  113. package/lib/typescript/src/index.d.ts +27 -0
  114. package/lib/typescript/src/index.d.ts.map +1 -0
  115. package/lib/typescript/src/store/themeStore.d.ts +32 -0
  116. package/lib/typescript/src/store/themeStore.d.ts.map +1 -0
  117. package/lib/typescript/src/theme/colors.d.ts +14 -0
  118. package/lib/typescript/src/theme/colors.d.ts.map +1 -0
  119. package/lib/typescript/src/theme/meassures.d.ts +9 -0
  120. package/lib/typescript/src/theme/meassures.d.ts.map +1 -0
  121. package/lib/typescript/src/utils/stringUtils.d.ts +7 -0
  122. package/lib/typescript/src/utils/stringUtils.d.ts.map +1 -0
  123. package/lib/typescript/src/utils/uiUtils.d.ts +21 -0
  124. package/lib/typescript/src/utils/uiUtils.d.ts.map +1 -0
  125. package/package.json +173 -0
  126. package/src/components/Accordion.tsx +284 -0
  127. package/src/components/BottomSheet.tsx +259 -0
  128. package/src/components/Button.tsx +85 -0
  129. package/src/components/ButtonContainer.tsx +161 -0
  130. package/src/components/CalendarPicker.tsx +428 -0
  131. package/src/components/Card.tsx +55 -0
  132. package/src/components/Checkbox.tsx +160 -0
  133. package/src/components/Chip.tsx +58 -0
  134. package/src/components/CopyToClipboard.tsx +108 -0
  135. package/src/components/Dialog.tsx +263 -0
  136. package/src/components/Header.tsx +100 -0
  137. package/src/components/Icon.tsx +27 -0
  138. package/src/components/IconButton.tsx +71 -0
  139. package/src/components/Input.tsx +144 -0
  140. package/src/components/InputContainer.tsx +134 -0
  141. package/src/components/LoadingIndicator.tsx +78 -0
  142. package/src/components/OtpInput.tsx +109 -0
  143. package/src/components/OtpInputContainer.tsx +196 -0
  144. package/src/components/Select.tsx +219 -0
  145. package/src/components/Switch.tsx +104 -0
  146. package/src/components/Tabs.tsx +117 -0
  147. package/src/components/Text.tsx +64 -0
  148. package/src/components/TextArea.tsx +141 -0
  149. package/src/hooks/useTheme.tsx +23 -0
  150. package/src/index.tsx +38 -0
  151. package/src/store/themeStore.ts +57 -0
  152. package/src/theme/colors.ts +35 -0
  153. package/src/theme/meassures.ts +7 -0
  154. package/src/utils/stringUtils.ts +16 -0
  155. package/src/utils/uiUtils.ts +70 -0
@@ -0,0 +1,71 @@
1
+ import { useMemo } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ type PressableProps,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ } from 'react-native';
8
+
9
+ import ButtonContainer, {
10
+ type ButtonVariant,
11
+ type ButtonSize,
12
+ type ButtonColors,
13
+ } from './ButtonContainer';
14
+ import Icon, { type IconName } from './Icon';
15
+ import LoadingIndicator from './LoadingIndicator';
16
+
17
+ import meassures from '../theme/meassures';
18
+
19
+ type Props = PressableProps & {
20
+ iconName: IconName;
21
+ variant?: keyof typeof ButtonVariant;
22
+ size?: keyof typeof ButtonSize;
23
+ loading?: boolean;
24
+ style?: StyleProp<ViewStyle>;
25
+ };
26
+
27
+ const PADDING = 8;
28
+
29
+ export default function IconButton({
30
+ iconName,
31
+ onPress,
32
+ size = 'medium',
33
+ disabled = false,
34
+ variant = 'flat',
35
+ loading = false,
36
+ style,
37
+ ...rest
38
+ }: Props) {
39
+ const iconSize = useMemo(
40
+ () => meassures.button[size] - (size === 'small' ? PADDING : PADDING * 2),
41
+ [size]
42
+ );
43
+
44
+ return (
45
+ <ButtonContainer
46
+ {...rest}
47
+ variant={variant}
48
+ size={size}
49
+ loading={loading}
50
+ disabled={disabled}
51
+ onPress={onPress}
52
+ hitSlop={{ top: PADDING, left: PADDING, right: PADDING, bottom: PADDING }}
53
+ style={[style, { width: meassures.button[size] }]}
54
+ contentContainerStyle={[
55
+ styles.container,
56
+ { padding: size === 'small' ? PADDING / 2 : PADDING },
57
+ ]}
58
+ renderContent={(colors: ButtonColors) =>
59
+ loading ? (
60
+ <LoadingIndicator size={iconSize} color={colors.textColor} />
61
+ ) : (
62
+ <Icon name={iconName} size={iconSize} color={colors.textColor} />
63
+ )
64
+ }
65
+ />
66
+ );
67
+ }
68
+
69
+ const styles = StyleSheet.create({
70
+ container: { alignItems: 'center', justifyContent: 'center' },
71
+ });
@@ -0,0 +1,144 @@
1
+ import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
2
+ import {
3
+ TextInput,
4
+ StyleSheet,
5
+ type StyleProp,
6
+ type ViewStyle,
7
+ type TextInputProps,
8
+ type TextInputFocusEventData,
9
+ type NativeSyntheticEvent,
10
+ Platform,
11
+ } from 'react-native';
12
+
13
+ import InputContainer from './InputContainer';
14
+ import IconButton from './IconButton';
15
+ import { type IconName } from './Icon';
16
+
17
+ import useTheme from '../hooks/useTheme';
18
+
19
+ import { convertHexToRgba } from '../utils/uiUtils';
20
+
21
+ type Props = TextInputProps & {
22
+ style?: StyleProp<ViewStyle>;
23
+ error?: boolean;
24
+ label?: string;
25
+ hint?: string;
26
+ iconName?: IconName;
27
+ variant?: 'text' | 'password';
28
+ clearable?: boolean;
29
+ };
30
+
31
+ export type InputRef = { focus: () => void; blur: () => void };
32
+
33
+ export const Input = forwardRef<InputRef, Props>(
34
+ (
35
+ {
36
+ style,
37
+ onChangeText,
38
+ value,
39
+ editable = true,
40
+ error,
41
+ label,
42
+ hint,
43
+ variant = 'text',
44
+ onBlur,
45
+ onFocus,
46
+ iconName,
47
+ clearable = false,
48
+ ...rest
49
+ },
50
+ ref
51
+ ) => {
52
+ const [focused, setFocused] = useState(false);
53
+ const [showPassword, setShowPassword] = useState(false);
54
+ const { colors, theme } = useTheme();
55
+ const inputRef = useRef<TextInput>(null);
56
+
57
+ useImperativeHandle(ref, () => ({
58
+ focus: () => inputRef.current?.focus(),
59
+ blur: () => inputRef.current?.blur(),
60
+ }));
61
+
62
+ const handleFocus = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
63
+ setFocused(true);
64
+ onFocus?.(e);
65
+ };
66
+
67
+ const handleBlur = (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
68
+ setFocused(false);
69
+ onBlur?.(e);
70
+ };
71
+
72
+ const renderRightButton = (name: IconName, onPress: () => void) => (
73
+ <IconButton
74
+ style={styles.button}
75
+ iconName={name}
76
+ size="small"
77
+ variant="transparent"
78
+ onPress={onPress}
79
+ disabled={!editable}
80
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
81
+ />
82
+ );
83
+
84
+ return (
85
+ <InputContainer
86
+ style={style}
87
+ label={label}
88
+ hint={hint}
89
+ error={error}
90
+ onPress={() => inputRef.current?.focus()}
91
+ contentContainerStyle={styles.container}
92
+ focused={focused}
93
+ disabled={!editable}
94
+ iconName={iconName}
95
+ >
96
+ <TextInput
97
+ {...rest}
98
+ value={value}
99
+ onChangeText={onChangeText}
100
+ ref={inputRef}
101
+ style={[
102
+ styles.input,
103
+ {
104
+ fontFamily: theme.fonts.regular,
105
+ color: colors.foreground,
106
+ // @ts-ignore
107
+ caretColor: colors.primary, // This to make the cursor color match the primary color on web
108
+ },
109
+ Platform.OS === 'web' && styles.webInput,
110
+ ]}
111
+ onFocus={handleFocus}
112
+ onBlur={handleBlur}
113
+ placeholderTextColor={convertHexToRgba(colors.foreground, 0.5)}
114
+ editable={editable}
115
+ secureTextEntry={variant === 'password' && !showPassword}
116
+ cursorColor={colors.primary}
117
+ selectionColor={convertHexToRgba(
118
+ colors.primary,
119
+ Platform.OS === 'android' ? 0.15 : 1
120
+ )}
121
+ selectionHandleColor={colors.primary}
122
+ />
123
+ {clearable &&
124
+ value &&
125
+ variant !== 'password' &&
126
+ renderRightButton('CircleX', () => onChangeText?.(''))}
127
+ {variant === 'password' &&
128
+ renderRightButton(showPassword ? 'EyeClosed' : 'Eye', () =>
129
+ setShowPassword(!showPassword)
130
+ )}
131
+ </InputContainer>
132
+ );
133
+ }
134
+ );
135
+
136
+ const styles = StyleSheet.create({
137
+ container: { flexDirection: 'row', alignItems: 'center' },
138
+ input: { flex: 1, fontSize: 14 },
139
+ button: { marginStart: 8, opacity: 0.8 },
140
+ icon: { marginEnd: 8 },
141
+ webInput: { outlineWidth: 0 } as ViewStyle, // To remove native focus outline on web
142
+ });
143
+
144
+ export default Input;
@@ -0,0 +1,134 @@
1
+ import { type PropsWithChildren, useEffect, useMemo } from 'react';
2
+ import {
3
+ Pressable,
4
+ StyleSheet,
5
+ View,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import Animated, {
10
+ interpolate,
11
+ interpolateColor,
12
+ useAnimatedStyle,
13
+ useSharedValue,
14
+ withTiming,
15
+ } from 'react-native-reanimated';
16
+
17
+ import Text from './Text';
18
+ import Icon, { type IconName } from './Icon';
19
+ import useTheme from '../hooks/useTheme';
20
+ import { convertHexToRgba } from '../utils/uiUtils';
21
+
22
+ type Props = PropsWithChildren & {
23
+ style?: StyleProp<ViewStyle>;
24
+ contentContainerStyle?: StyleProp<ViewStyle>;
25
+ error?: boolean;
26
+ hint?: string;
27
+ label?: string;
28
+ onPress: () => void;
29
+ focused?: boolean;
30
+ disabled?: boolean;
31
+ size?: 'small' | 'default';
32
+ height?: number;
33
+ iconName?: IconName;
34
+ };
35
+
36
+ export type InputRef = { focus: () => void };
37
+
38
+ const INACTIVE = -1;
39
+ const ACTIVE = 0;
40
+ const ERROR = 1;
41
+
42
+ export default function InputContainer({
43
+ style,
44
+ contentContainerStyle,
45
+ error,
46
+ hint,
47
+ label,
48
+ onPress,
49
+ focused,
50
+ children,
51
+ size = 'default',
52
+ disabled,
53
+ height,
54
+ iconName,
55
+ }: Props) {
56
+ const status = useSharedValue(INACTIVE);
57
+ const isFocused = useSharedValue(0);
58
+ const { colors, theme } = useTheme();
59
+
60
+ const animStyle = useAnimatedStyle(() => ({
61
+ borderColor: interpolateColor(
62
+ status.value,
63
+ [INACTIVE, ACTIVE, ERROR],
64
+ [colors.border, colors.primary, colors.destructive]
65
+ ),
66
+ }));
67
+
68
+ const iconAnimStyle = useAnimatedStyle(() => {
69
+ return {
70
+ opacity: interpolate(isFocused.value, [0, 1], [1, 0]),
71
+ width: interpolate(isFocused.value, [0, 1], [24, 0]),
72
+ };
73
+ });
74
+
75
+ useEffect(() => {
76
+ const _status = focused ? ACTIVE : INACTIVE;
77
+ status.value = withTiming(error ? ERROR : _status, { duration: 300 });
78
+ isFocused.value = withTiming(focused ? 1 : 0, { duration: 300 });
79
+ }, [error, status, focused, isFocused]);
80
+
81
+ const containerHeight = useMemo(
82
+ () => height ?? (size === 'small' ? 32 : 40),
83
+ [size, height]
84
+ );
85
+
86
+ return (
87
+ <View style={[style, disabled && styles.disabled]}>
88
+ {!!label && (
89
+ <Text style={[{ color: colors.foreground }, styles.label]} variant="h5">
90
+ {label}
91
+ </Text>
92
+ )}
93
+ <Pressable onPress={onPress} disabled={disabled}>
94
+ <Animated.View
95
+ style={[
96
+ animStyle,
97
+ styles.container,
98
+ { height: containerHeight, borderRadius: theme.roundness },
99
+ contentContainerStyle,
100
+ ]}
101
+ >
102
+ {iconName && (
103
+ <Animated.View style={iconAnimStyle}>
104
+ <Icon
105
+ name={iconName}
106
+ size={16}
107
+ color={convertHexToRgba(colors.foreground, 0.5)}
108
+ />
109
+ </Animated.View>
110
+ )}
111
+ {children}
112
+ </Animated.View>
113
+ </Pressable>
114
+ {((hint && !error) || (error && hint)) && (
115
+ <Text style={styles.hint} variant="caption">
116
+ {hint}
117
+ </Text>
118
+ )}
119
+ </View>
120
+ );
121
+ }
122
+
123
+ const styles = StyleSheet.create({
124
+ hint: { marginHorizontal: 8, marginTop: 4 },
125
+ container: {
126
+ flexDirection: 'row',
127
+ alignItems: 'center',
128
+ overflow: 'hidden',
129
+ paddingHorizontal: 12,
130
+ borderWidth: 1,
131
+ },
132
+ disabled: { opacity: 0.5 },
133
+ label: { marginBottom: 4 },
134
+ });
@@ -0,0 +1,78 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Platform,
5
+ StyleSheet,
6
+ View,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import Animated, {
11
+ useAnimatedStyle,
12
+ withRepeat,
13
+ withTiming,
14
+ useSharedValue,
15
+ Easing,
16
+ } from 'react-native-reanimated';
17
+
18
+ import useTheme from '../hooks/useTheme';
19
+
20
+ type Props = {
21
+ style?: StyleProp<ViewStyle>;
22
+ color?: string;
23
+ size?: number;
24
+ };
25
+
26
+ export default function LoadingIndicator({ style, color, size = 24 }: Props) {
27
+ const rotation = useSharedValue(0);
28
+ const { colors } = useTheme();
29
+ const borderColor = color ?? colors.foreground;
30
+
31
+ useEffect(() => {
32
+ rotation.value = withRepeat(
33
+ withTiming(360, { duration: 1000, easing: Easing.linear }),
34
+ -1,
35
+ false
36
+ );
37
+ }, [rotation]);
38
+
39
+ const animatedStyle = useAnimatedStyle(() => {
40
+ return { transform: [{ rotate: `${rotation.value}deg` }] };
41
+ });
42
+
43
+ return (
44
+ <View style={[styles.container, { width: size, height: size }, style]}>
45
+ {/* Workaround for Android because of this issue: https://github.com/facebook/react-native/issues/38335 */}
46
+ {Platform.OS === 'android' ? (
47
+ <ActivityIndicator size={size} color={borderColor} />
48
+ ) : (
49
+ <Animated.View
50
+ style={[
51
+ styles.circle,
52
+ {
53
+ borderTopColor: borderColor,
54
+ borderLeftColor: borderColor,
55
+ borderBottomColor: borderColor,
56
+ },
57
+ animatedStyle,
58
+ ]}
59
+ />
60
+ )}
61
+ </View>
62
+ );
63
+ }
64
+
65
+ const styles = StyleSheet.create({
66
+ container: {
67
+ justifyContent: 'center',
68
+ alignItems: 'center',
69
+ },
70
+ circle: {
71
+ width: '100%',
72
+ height: '100%',
73
+ borderRadius: 999,
74
+ borderWidth: 2,
75
+ borderStyle: 'solid',
76
+ borderRightColor: 'transparent',
77
+ },
78
+ });
@@ -0,0 +1,109 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useImperativeHandle,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ type NativeSyntheticEvent,
10
+ StyleSheet,
11
+ TextInput,
12
+ type TextInputProps,
13
+ type TextInputKeyPressEventData,
14
+ } from 'react-native';
15
+ import { convertHexToRgba } from '../utils/uiUtils';
16
+ import useTheme from '../hooks/useTheme';
17
+
18
+ export const OtpInput = forwardRef<TextInput, TextInputProps>(
19
+ (
20
+ {
21
+ placeholder = '*',
22
+ style,
23
+ onChangeText,
24
+ onKeyPress,
25
+ editable = true,
26
+ ...rest
27
+ }: TextInputProps,
28
+ ref
29
+ ) => {
30
+ const innerRef = useRef<TextInput>(null);
31
+ const [value, setValue] = useState<string>('');
32
+ const [innerPlaceholder, setInnerPlaceholder] =
33
+ useState<string>(placeholder);
34
+ const { colors } = useTheme();
35
+
36
+ useImperativeHandle(ref, () => innerRef.current as TextInput);
37
+
38
+ const onFocus = useCallback(() => setInnerPlaceholder(''), []);
39
+
40
+ const onBlur = useCallback(() => {
41
+ if (!value?.length) {
42
+ setInnerPlaceholder(placeholder);
43
+ }
44
+ }, [placeholder, value?.length]);
45
+
46
+ const handleChangeText = useCallback(
47
+ (text: string) => {
48
+ const regex = /^\d+$/;
49
+ if (regex.test(text)) {
50
+ setValue(text);
51
+ onChangeText?.(text);
52
+ } else {
53
+ innerRef.current?.setNativeProps({ text: '' });
54
+ }
55
+ },
56
+ [onChangeText]
57
+ );
58
+
59
+ const handleKeyPress = useCallback(
60
+ (event: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
61
+ if (event.nativeEvent.key === 'Backspace') {
62
+ setValue('');
63
+ innerRef.current?.setNativeProps({ text: '' });
64
+ if (!innerRef?.current?.isFocused()) {
65
+ innerRef.current?.setNativeProps({ placeholder: innerPlaceholder });
66
+ }
67
+ }
68
+ onKeyPress?.(event);
69
+ },
70
+ [innerPlaceholder, onKeyPress]
71
+ );
72
+
73
+ return (
74
+ <TextInput
75
+ ref={innerRef}
76
+ {...rest}
77
+ placeholderTextColor={convertHexToRgba(colors.foreground, 0.5)}
78
+ style={[
79
+ styles.container,
80
+ style,
81
+ { backgroundColor: colors.background },
82
+ ]}
83
+ placeholder={innerPlaceholder}
84
+ onFocus={onFocus}
85
+ onBlur={onBlur}
86
+ onChangeText={handleChangeText}
87
+ maxLength={1}
88
+ onKeyPress={handleKeyPress}
89
+ keyboardType="number-pad"
90
+ editable={editable}
91
+ textAlignVertical="center"
92
+ textAlign="center"
93
+ />
94
+ );
95
+ }
96
+ );
97
+
98
+ const styles = StyleSheet.create({
99
+ container: {
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ fontSize: 10,
103
+ fontWeight: 'normal',
104
+ borderWidth: 1,
105
+ borderRadius: 8,
106
+ height: 71,
107
+ width: 80,
108
+ },
109
+ });