diff --git a/src/components/caret.tsx b/src/components/caret.tsx index 6a56278..ba2e482 100644 --- a/src/components/caret.tsx +++ b/src/components/caret.tsx @@ -1,11 +1,17 @@ import { useEffect, useRef } from 'react'; -import { StyleSheet, Animated } from 'react-native'; -import { DEFAULT_DARK_COLOR } from '../constants'; +import { StyleSheet, Animated, Platform } from 'react-native'; import { useTextInputOTP } from '../hooks/use-text-input-otp'; +import { useThemeColor } from '../hooks/use-theme-color'; +import { theme } from '../theme'; export function Caret() { const opacity = useRef(new Animated.Value(0)).current; + const useNativeDriver = Platform.OS === 'ios' || Platform.OS === 'android'; const { caretColor } = useTextInputOTP(); + const defaultBackgroundColor = useThemeColor({ + light: theme.colorBlack, + dark: theme.colorWhite, + }); useEffect(() => { Animated.loop( @@ -13,12 +19,12 @@ export function Caret() { Animated.timing(opacity, { toValue: 0, duration: 500, - useNativeDriver: true, + useNativeDriver: false, }), Animated.timing(opacity, { toValue: 1, duration: 500, - useNativeDriver: true, + useNativeDriver, }), ]) ).start(); @@ -29,7 +35,7 @@ export function Caret() { testID="caret" style={[ styles.caret, - { opacity, backgroundColor: caretColor ?? DEFAULT_DARK_COLOR }, + { opacity, backgroundColor: caretColor ?? defaultBackgroundColor }, ]} /> ); @@ -37,8 +43,8 @@ export function Caret() { const styles = StyleSheet.create({ caret: { - width: 2, - height: 16, - borderRadius: 16, + width: theme.space2, + height: theme.space16, + borderRadius: theme.borderRadius16, }, }); diff --git a/src/components/text-input-otp-separator.tsx b/src/components/text-input-otp-separator.tsx index ade0d62..b0b06d2 100644 --- a/src/components/text-input-otp-separator.tsx +++ b/src/components/text-input-otp-separator.tsx @@ -1,20 +1,31 @@ import { View, StyleSheet } from 'react-native'; -import { DEFAULT_DARK_COLOR } from '../constants'; import type { TextInputOTPSeparatorProps } from '../types'; +import { useThemeColor } from '../hooks/use-theme-color'; +import { theme } from '../theme'; export function TextInputOTPSeparator({ separatorStyles, }: TextInputOTPSeparatorProps) { + const defaultBackgroundColor = useThemeColor({ + light: theme.colorBlack, + dark: theme.colorWhite, + }); + return ( - + ); } const styles = StyleSheet.create({ separator: { - width: 10, - height: 4, - backgroundColor: DEFAULT_DARK_COLOR, - borderRadius: 15, + width: theme.space10, + height: theme.space4, + borderRadius: theme.space16, }, }); diff --git a/src/components/text-input-otp-slot.tsx b/src/components/text-input-otp-slot.tsx index 00a8054..d0622f8 100644 --- a/src/components/text-input-otp-slot.tsx +++ b/src/components/text-input-otp-slot.tsx @@ -1,13 +1,15 @@ import { memo } from 'react'; import { Pressable, Text, StyleSheet } from 'react-native'; -import { Caret } from './caret'; import { useTextInputOTP } from '../hooks/use-text-input-otp'; import { useSlotBorderStyles } from '../hooks/use-slot-border-styles'; -import { DEFAULT_DARK_COLOR, SLOT_HEIGHT, SLOT_WIDTH } from '../constants'; +import { useThemeColor } from '../hooks/use-theme-color'; +import { SLOT_HEIGHT, SLOT_WIDTH } from '../constants'; import type { TextInputOTPSlotInternalProps, TextInputOTPSlotProps, } from '../types'; +import { theme } from '../theme'; +import { Caret } from './caret'; function TextInputOTPSlotComponent({ index, @@ -22,6 +24,11 @@ function TextInputOTPSlotComponent({ const { code, currentIndex, handlePress, caretHidden } = useTextInputOTP(); const isFocused = currentIndex === index; const borderStyles = useSlotBorderStyles({ isFocused, isFirst, isLast }); + const defaultTextColor = useThemeColor({ + light: theme.colorBlack, + dark: theme.colorWhite, + }); + const shouldRenderCaret = isFocused && !code[index] && !caretHidden; return ( @@ -39,6 +46,7 @@ function TextInputOTPSlotComponent({ @@ -61,8 +69,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, slotText: { - color: DEFAULT_DARK_COLOR, - fontSize: 14, - fontWeight: 'bold', + fontSize: theme.fontSize14, + fontWeight: theme.fontWeightBold, }, }); diff --git a/src/components/text-input-otp.tsx b/src/components/text-input-otp.tsx index d8679b3..4ea3dd7 100644 --- a/src/components/text-input-otp.tsx +++ b/src/components/text-input-otp.tsx @@ -1,8 +1,9 @@ +import { forwardRef } from 'react'; import { View, StyleSheet } from 'react-native'; import { TextInputOTPProvider } from '../hooks/use-text-input-otp'; -import { TextInput } from './text-input'; -import { forwardRef } from 'react'; import type { TextInputOTPProps, TextInputOTPRef } from '../types'; +import { theme } from '../theme'; +import { TextInput } from './text-input'; export const TextInputOTP = forwardRef( ({ children, containerStyles, ...rest }, ref) => { @@ -22,6 +23,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - gap: 10, + gap: theme.space10, }, }); diff --git a/src/constants.ts b/src/constants.ts index b22701d..96b8c3f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,3 @@ export const SLOT_WIDTH = 50; export const SLOT_HEIGHT = 50; export const FOCUSED_SLOT_HEIGHT = 54; -export const DEFAULT_DARK_COLOR = '#030712'; -export const DEFAULT_LIGHT_COLOR = '#E4E7EC'; diff --git a/src/hooks/use-slot-border-styles.ts b/src/hooks/use-slot-border-styles.ts index a8c3d11..fe739be 100644 --- a/src/hooks/use-slot-border-styles.ts +++ b/src/hooks/use-slot-border-styles.ts @@ -1,26 +1,33 @@ -import { - DEFAULT_DARK_COLOR, - DEFAULT_LIGHT_COLOR, - FOCUSED_SLOT_HEIGHT, - SLOT_HEIGHT, -} from '../constants'; +import type { StyleProp, ViewStyle } from 'react-native'; +import { FOCUSED_SLOT_HEIGHT, SLOT_HEIGHT } from '../constants'; +import { theme } from '../theme'; import type { UseSlotBorderStylesProps } from '../types'; +import { useThemeColor } from './use-theme-color'; export function useSlotBorderStyles({ isFocused, isFirst, isLast, -}: UseSlotBorderStylesProps) { +}: UseSlotBorderStylesProps): StyleProp { + const darkBorder = useThemeColor({ + light: theme.colorBlack, + dark: theme.colorWhite, + }); + const lightBorder = useThemeColor({ + light: theme.colorLightGrey, + dark: theme.colorDarkGrey, + }); + return { height: isFocused ? FOCUSED_SLOT_HEIGHT : SLOT_HEIGHT, - borderColor: isFocused ? DEFAULT_DARK_COLOR : DEFAULT_LIGHT_COLOR, - borderTopWidth: 2, - borderBottomWidth: 2, - borderLeftWidth: isFocused || isFirst ? 2 : 1, - borderRightWidth: isFocused || isLast ? 2 : 1, - borderTopLeftRadius: isFirst ? 8 : 0, - borderTopRightRadius: isLast ? 8 : 0, - borderBottomLeftRadius: isFirst ? 8 : 0, - borderBottomRightRadius: isLast ? 8 : 0, + borderColor: isFocused ? darkBorder : lightBorder, + borderTopWidth: theme.space2, + borderBottomWidth: theme.space2, + borderLeftWidth: isFocused || isFirst ? theme.space2 : theme.space1, + borderRightWidth: isFocused || isLast ? theme.space2 : theme.space1, + borderTopLeftRadius: isFirst ? theme.borderRadius8 : theme.borderRadius0, + borderTopRightRadius: isLast ? theme.borderRadius8 : theme.borderRadius0, + borderBottomLeftRadius: isFirst ? theme.borderRadius8 : theme.borderRadius0, + borderBottomRightRadius: isLast ? theme.borderRadius8 : theme.borderRadius0, }; } diff --git a/src/hooks/use-theme-color.ts b/src/hooks/use-theme-color.ts new file mode 100644 index 0000000..e34d4aa --- /dev/null +++ b/src/hooks/use-theme-color.ts @@ -0,0 +1,6 @@ +import { useColorScheme } from 'react-native'; + +export function useThemeColor(props: { light: T; dark: U }) { + const theme = useColorScheme() ?? 'light'; + return props[theme]; +} diff --git a/src/theme.ts b/src/theme.ts new file mode 100644 index 0000000..8b3bac8 --- /dev/null +++ b/src/theme.ts @@ -0,0 +1,20 @@ +export const theme = { + colorBlack: '#030712', + colorWhite: '#E4E7EC', + colorLightGrey: '#E4E7EC', + colorDarkGrey: '#4b5563', + + fontSize14: 14, + + fontWeightBold: 'bold' as 'bold', + + space1: 1, + space2: 2, + space4: 4, + space10: 10, + space16: 16, + + borderRadius0: 0, + borderRadius8: 8, + borderRadius16: 16, +};