@oneplatformdev/react-native-uikit 0.0.0-stage → 1.1.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 (68) hide show
  1. package/README.md +376 -2
  2. package/bin/cli.cjs +372 -0
  3. package/package.json +34 -4
  4. package/registry/manifest.json +113 -0
  5. package/src/components/BottomModal/index.ts +2 -0
  6. package/src/components/BottomModal/presenters/useBottomModal.ts +114 -0
  7. package/src/components/BottomModal/presenters/useBottomModalInsets.ts +19 -0
  8. package/src/components/BottomModal/types/types.ts +23 -0
  9. package/src/components/BottomModal/ui/BottomModal.tsx +110 -0
  10. package/src/components/BottomModal/ui/styles.ts +115 -0
  11. package/src/components/Button/index.ts +8 -0
  12. package/src/components/Button/presenters/useButton.ts +72 -0
  13. package/src/components/Button/types/types.ts +39 -0
  14. package/src/components/Button/ui/Button.tsx +121 -0
  15. package/src/components/Button/ui/styles.ts +82 -0
  16. package/src/components/ConnectionContainer/index.ts +2 -0
  17. package/src/components/ConnectionContainer/presenters/useConnectionContainer.ts +77 -0
  18. package/src/components/ConnectionContainer/types/types.ts +14 -0
  19. package/src/components/ConnectionContainer/ui/ConnectionContainer.tsx +51 -0
  20. package/src/components/ConnectionContainer/ui/styles.ts +35 -0
  21. package/src/components/CustomAlert/index.ts +2 -0
  22. package/src/components/CustomAlert/presenters/useCustomAlert.ts +24 -0
  23. package/src/components/CustomAlert/types/types.ts +20 -0
  24. package/src/components/CustomAlert/ui/CustomAlert.tsx +77 -0
  25. package/src/components/CustomAlert/ui/styles.ts +71 -0
  26. package/src/components/HeaderWithBackButton/index.ts +2 -0
  27. package/src/components/HeaderWithBackButton/presenters/useHeaderWithBackButton.ts +17 -0
  28. package/src/components/HeaderWithBackButton/types/types.ts +21 -0
  29. package/src/components/HeaderWithBackButton/ui/HeaderWithBackButton.tsx +90 -0
  30. package/src/components/HeaderWithBackButton/ui/styles.ts +58 -0
  31. package/src/components/Input/index.ts +8 -0
  32. package/src/components/Input/presenters/useInput.ts +104 -0
  33. package/src/components/Input/types/types.ts +36 -0
  34. package/src/components/Input/ui/Input.tsx +161 -0
  35. package/src/components/Input/ui/styles.ts +80 -0
  36. package/src/components/Loader/index.ts +2 -0
  37. package/src/components/Loader/types/types.ts +12 -0
  38. package/src/components/Loader/ui/Loader.tsx +29 -0
  39. package/src/components/Loader/ui/styles.ts +14 -0
  40. package/src/components/Logger/index.ts +2 -0
  41. package/src/components/Logger/presenters/useLogger.ts +13 -0
  42. package/src/components/Logger/types/types.ts +56 -0
  43. package/src/components/Logger/ui/Logger.tsx +85 -0
  44. package/src/components/Logger/ui/components/LoggerItem.tsx +117 -0
  45. package/src/components/Logger/ui/styles.ts +121 -0
  46. package/src/components/Logger/utils/groupLoggerEntries.ts +40 -0
  47. package/src/components/Logger/utils/loggerPresentation.ts +69 -0
  48. package/src/components/ScreenContainer/index.ts +2 -0
  49. package/src/components/ScreenContainer/presenters/useScreenContainer.ts +12 -0
  50. package/src/components/ScreenContainer/types/types.ts +19 -0
  51. package/src/components/ScreenContainer/ui/ScreenContainer.tsx +83 -0
  52. package/src/components/ScreenContainer/ui/styles.ts +31 -0
  53. package/src/components/Typography/index.ts +2 -0
  54. package/src/components/Typography/types/types.ts +14 -0
  55. package/src/components/Typography/ui/Typography.tsx +37 -0
  56. package/src/components/Typography/ui/styles.ts +26 -0
  57. package/src/index.ts +13 -0
  58. package/src/provider/LocalizationContext.tsx +14 -0
  59. package/src/provider/ModeContext.tsx +9 -0
  60. package/src/provider/index.ts +12 -0
  61. package/src/provider/types.ts +31 -0
  62. package/src/theme/ThemeProvider.tsx +7 -0
  63. package/src/theme/UIProvider.tsx +60 -0
  64. package/src/theme/createTheme.ts +66 -0
  65. package/src/theme/defaultTheme.ts +215 -0
  66. package/src/theme/index.ts +4 -0
  67. package/src/theme/types.ts +265 -0
  68. package/src/utils/index.ts +94 -0
@@ -0,0 +1,51 @@
1
+ import React, { useMemo } from 'react';
2
+ import { TouchableOpacity, View } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
4
+ import { Typography } from '../../Typography';
5
+ import { useUITheme } from '../../../theme';
6
+ import { useScaling } from '../../../utils';
7
+ import { getStyles } from './styles';
8
+ import type { ConnectionContainerProps } from '../types/types';
9
+ import { useConnectionContainer } from '../presenters/useConnectionContainer';
10
+
11
+ export const ConnectionContainer = ({
12
+ isConnected,
13
+ disconnectedText,
14
+ reconnectedText,
15
+ children,
16
+ disconnectedIcon,
17
+ reconnectedIcon,
18
+ dismissAccessibilityHint,
19
+ style,
20
+ }: ConnectionContainerProps) => {
21
+ const { colors, spacing } = useUITheme();
22
+ const scaling = useScaling();
23
+ const styles = useMemo(() => getStyles(colors, spacing, scaling), [colors, scaling, spacing]);
24
+ const { visible, isReconnected, text, onHideBanner } = useConnectionContainer({
25
+ isConnected,
26
+ disconnectedText,
27
+ reconnectedText,
28
+ });
29
+
30
+ if (!visible) return children == null ? null : <>{children}</>;
31
+
32
+ return (
33
+ <>
34
+ {children}
35
+ <SafeAreaView edges={['top', 'left', 'right']} pointerEvents="box-none" style={[styles.container, style]}>
36
+ <TouchableOpacity
37
+ accessibilityRole="button"
38
+ accessibilityLabel={text}
39
+ accessibilityHint={dismissAccessibilityHint}
40
+ activeOpacity={0.8}
41
+ onPress={onHideBanner}
42
+ >
43
+ <View style={[styles.content, isReconnected ? styles.reconnected : styles.disconnected]}>
44
+ {isReconnected ? reconnectedIcon : disconnectedIcon}
45
+ <Typography text={text} variant="label" weight="bold" style={styles.text} accessibilityLiveRegion="polite" />
46
+ </View>
47
+ </TouchableOpacity>
48
+ </SafeAreaView>
49
+ </>
50
+ );
51
+ };
@@ -0,0 +1,35 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import type { UIColors, UISpacing } from '../../../theme';
3
+ import type { ScalingFunctions } from '../../../utils';
4
+
5
+ export const getStyles = (colors: UIColors, spacing: UISpacing, scaling: ScalingFunctions) => {
6
+ const styles = StyleSheet.create({
7
+ container: {
8
+ position: 'absolute',
9
+ top: 0,
10
+ left: 0,
11
+ right: 0,
12
+ zIndex: 10,
13
+ },
14
+ reconnected: {
15
+ backgroundColor: colors.success,
16
+ },
17
+ disconnected: {
18
+ backgroundColor: colors.error,
19
+ },
20
+ content: {
21
+ flexDirection: 'row',
22
+ alignItems: 'center',
23
+ justifyContent: 'center',
24
+ gap: scaling.scaleHorizontal(spacing.sm),
25
+ paddingVertical: scaling.scaleVertical(spacing.md),
26
+ paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
27
+ },
28
+ text: {
29
+ flexShrink: 1,
30
+ color: colors.onPrimary,
31
+ },
32
+ });
33
+
34
+ return styles;
35
+ };
@@ -0,0 +1,2 @@
1
+ export { CustomAlert } from './ui/CustomAlert';
2
+ export type { CustomAlertCloseAction, CustomAlertProps } from './types/types';
@@ -0,0 +1,24 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Animated } from 'react-native';
3
+ import type { CustomAlertProps } from '../types/types';
4
+
5
+ export const useCustomAlert = ({ visible, onClose }: Pick<CustomAlertProps, 'visible' | 'onClose'>) => {
6
+ const [isVisible, setIsVisible] = useState(visible);
7
+ const backdropOpacity = useRef(new Animated.Value(0)).current;
8
+ const scaleAnim = useRef(new Animated.Value(0.8)).current;
9
+ const onClosePress = useCallback(() => onClose(), [onClose]);
10
+
11
+ useEffect(() => {
12
+ if (visible) setIsVisible(true);
13
+ const animation = Animated.parallel([
14
+ Animated.timing(backdropOpacity, { toValue: visible ? 1 : 0, duration: 200, useNativeDriver: true }),
15
+ Animated.timing(scaleAnim, { toValue: visible ? 1 : 0.8, duration: 200, useNativeDriver: true }),
16
+ ]);
17
+ animation.start(({ finished }) => {
18
+ if (finished && !visible) setIsVisible(false);
19
+ });
20
+ return () => animation.stop();
21
+ }, [visible, backdropOpacity, scaleAnim]);
22
+
23
+ return { isVisible, backdropOpacity, scaleAnim, onClosePress };
24
+ };
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+
4
+ export interface CustomAlertCloseAction {
5
+ icon: ReactNode;
6
+ accessibilityLabel: string;
7
+ accessibilityHint?: string;
8
+ }
9
+
10
+ export interface CustomAlertProps {
11
+ visible: boolean;
12
+ onClose: () => void;
13
+ header?: ReactNode;
14
+ children?: ReactNode;
15
+ footer?: ReactNode;
16
+ showHandle?: boolean;
17
+ closeAction?: CustomAlertCloseAction;
18
+ style?: StyleProp<ViewStyle>;
19
+ contentStyle?: StyleProp<ViewStyle>;
20
+ }
@@ -0,0 +1,77 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Animated, Modal, Pressable, ScrollView, View } from 'react-native';
3
+ import { Typography } from '../../Typography';
4
+ import { useUITheme } from '../../../theme';
5
+ import { useScaling } from '../../../utils';
6
+ import { useCustomAlert } from '../presenters/useCustomAlert';
7
+ import { getAlertAnimatedStyles, getStyles } from './styles';
8
+ import type { CustomAlertProps } from '../types/types';
9
+
10
+ export const CustomAlert = ({
11
+ visible,
12
+ onClose,
13
+ header,
14
+ children,
15
+ footer,
16
+ showHandle = false,
17
+ closeAction,
18
+ style,
19
+ contentStyle,
20
+ }: CustomAlertProps) => {
21
+ const { colors, spacing, radius } = useUITheme();
22
+ const scaling = useScaling();
23
+ const styles = useMemo(() => getStyles(colors, spacing, radius, scaling), [colors, radius, scaling, spacing]);
24
+ const { isVisible, backdropOpacity, scaleAnim, onClosePress } = useCustomAlert({ visible, onClose });
25
+ const { backdropStyle, alertStyle } = useMemo(
26
+ () => getAlertAnimatedStyles(backdropOpacity, scaleAnim),
27
+ [backdropOpacity, scaleAnim],
28
+ );
29
+
30
+ if (!isVisible) return null;
31
+
32
+ return (
33
+ <Modal visible={isVisible} transparent animationType="none" statusBarTranslucent onRequestClose={onClosePress}>
34
+ <View style={styles.modalContainer}>
35
+ <Animated.View pointerEvents="none" style={[styles.backdrop, backdropStyle]} />
36
+ <Pressable
37
+ accessible={false}
38
+ importantForAccessibility="no"
39
+ onPress={onClosePress}
40
+ style={styles.backdropTouch}
41
+ />
42
+
43
+ <Animated.View
44
+ accessibilityViewIsModal
45
+ onAccessibilityEscape={onClosePress}
46
+ style={[styles.alertContainer, alertStyle, style]}
47
+ >
48
+ <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
49
+ {showHandle ? <View style={styles.handle} /> : null}
50
+ {header != null || closeAction ? (
51
+ <View style={styles.header}>
52
+ <View style={styles.headerRow}>
53
+ {typeof header === 'string' ? (
54
+ <Typography text={header} variant="title" style={styles.headerText} />
55
+ ) : header}
56
+ {closeAction ? (
57
+ <Pressable
58
+ accessibilityRole="button"
59
+ accessibilityLabel={closeAction.accessibilityLabel}
60
+ accessibilityHint={closeAction.accessibilityHint}
61
+ onPress={onClosePress}
62
+ style={styles.closeButton}
63
+ >
64
+ {closeAction.icon}
65
+ </Pressable>
66
+ ) : null}
67
+ </View>
68
+ </View>
69
+ ) : null}
70
+ {children != null ? <View style={[styles.content, contentStyle]}>{children}</View> : null}
71
+ {footer != null ? <View style={styles.footer}>{footer}</View> : null}
72
+ </ScrollView>
73
+ </Animated.View>
74
+ </View>
75
+ </Modal>
76
+ );
77
+ };
@@ -0,0 +1,71 @@
1
+ import { Animated, StyleSheet } from 'react-native';
2
+ import type { UIColors, UIRadius, UISpacing } from '../../../theme';
3
+ import type { ScalingFunctions } from '../../../utils';
4
+
5
+ export const getStyles = (colors: UIColors, spacing: UISpacing, radius: UIRadius, scaling: ScalingFunctions) => {
6
+ const styles = StyleSheet.create({
7
+ modalContainer: {
8
+ flex: 1,
9
+ justifyContent: 'center',
10
+ alignItems: 'center',
11
+ },
12
+ backdrop: {
13
+ ...StyleSheet.absoluteFill,
14
+ backgroundColor: colors.text,
15
+ },
16
+ backdropTouch: {
17
+ ...StyleSheet.absoluteFill,
18
+ },
19
+ alertContainer: {
20
+ width: '90%',
21
+ maxHeight: '85%',
22
+ backgroundColor: colors.background,
23
+ borderRadius: scaling.scaleHorizontal(radius.lg),
24
+ borderWidth: scaling.scaleHorizontal(1),
25
+ borderColor: colors.border,
26
+ overflow: 'hidden',
27
+ },
28
+ handle: {
29
+ width: scaling.scaleHorizontal(56),
30
+ height: scaling.scaleVertical(5),
31
+ borderRadius: scaling.scaleHorizontal(radius.full),
32
+ backgroundColor: colors.border,
33
+ alignSelf: 'center',
34
+ marginTop: scaling.scaleVertical(spacing.md),
35
+ },
36
+ header: {
37
+ paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
38
+ paddingVertical: scaling.scaleVertical(spacing.md),
39
+ },
40
+ headerRow: {
41
+ minHeight: scaling.scaleVertical(28),
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ },
45
+ headerText: {
46
+ paddingHorizontal: scaling.scaleHorizontal(36),
47
+ textAlign: 'center',
48
+ },
49
+ closeButton: {
50
+ position: 'absolute',
51
+ top: 0,
52
+ right: 0,
53
+ padding: scaling.scaleHorizontal(spacing.xs),
54
+ },
55
+ content: {
56
+ paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
57
+ },
58
+ footer: {
59
+ paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
60
+ paddingTop: scaling.scaleVertical(spacing.md),
61
+ paddingBottom: scaling.scaleVertical(spacing.xl),
62
+ },
63
+ });
64
+
65
+ return styles;
66
+ };
67
+
68
+ export const getAlertAnimatedStyles = (backdropOpacity: Animated.Value, scaleAnim: Animated.Value) => ({
69
+ backdropStyle: { opacity: backdropOpacity.interpolate({ inputRange: [0, 1], outputRange: [0, 0.5] }) },
70
+ alertStyle: { opacity: backdropOpacity, transform: [{ scale: scaleAnim }] },
71
+ });
@@ -0,0 +1,2 @@
1
+ export { HeaderWithBackButton } from './ui/HeaderWithBackButton';
2
+ export type { HeaderWithBackButtonProps } from './types/types';
@@ -0,0 +1,17 @@
1
+ import { useState } from 'react';
2
+ import type { HeaderWithBackButtonProps } from '../types/types';
3
+
4
+ export const useHeaderWithBackButton = (titleAlign: HeaderWithBackButtonProps['titleAlign']) => {
5
+ const [leftWidth, setLeftWidth] = useState<number>();
6
+ const [rightWidth, setRightWidth] = useState<number>();
7
+ const centered = titleAlign === 'center';
8
+
9
+ return {
10
+ centered,
11
+ leftWidth,
12
+ rightWidth,
13
+ setLeftWidth,
14
+ setRightWidth,
15
+ sideWidth: Math.max(leftWidth ?? 0, rightWidth ?? 0),
16
+ };
17
+ };
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { TypographyProps } from '../../Typography';
4
+
5
+ export interface HeaderWithBackButtonProps {
6
+ title: string | number;
7
+ subtitle?: string;
8
+ onBackPress: () => void;
9
+ backIcon: ReactNode;
10
+ backAccessibilityLabel: string;
11
+ backAccessibilityHint?: string;
12
+ backDisabled?: boolean;
13
+ rightAccessory?: ReactNode;
14
+ titleAlign?: 'left' | 'center';
15
+ titleVariant?: TypographyProps['variant'];
16
+ titleSize?: TypographyProps['size'];
17
+ subtitleVariant?: TypographyProps['variant'];
18
+ subtitleSize?: TypographyProps['size'];
19
+ style?: StyleProp<ViewStyle>;
20
+ contentStyle?: StyleProp<ViewStyle>;
21
+ }
@@ -0,0 +1,90 @@
1
+ import React, { useMemo } from 'react';
2
+ import { TouchableOpacity, View } from 'react-native';
3
+ import { useUITheme } from '../../../theme';
4
+ import { useScaling } from '../../../utils';
5
+ import { Typography } from '../../Typography';
6
+ import { useHeaderWithBackButton } from '../presenters/useHeaderWithBackButton';
7
+ import { getStyles } from './styles';
8
+ import type { HeaderWithBackButtonProps } from '../types/types';
9
+
10
+ export const HeaderWithBackButton = ({
11
+ title,
12
+ subtitle,
13
+ onBackPress,
14
+ backIcon,
15
+ backAccessibilityLabel,
16
+ backAccessibilityHint,
17
+ backDisabled = false,
18
+ rightAccessory,
19
+ titleAlign = 'left',
20
+ titleVariant = 'h3',
21
+ titleSize,
22
+ subtitleVariant = 'caption',
23
+ subtitleSize,
24
+ style,
25
+ contentStyle,
26
+ }: HeaderWithBackButtonProps) => {
27
+ const { colors, spacing } = useUITheme();
28
+ const scaling = useScaling();
29
+ const styles = useMemo(() => getStyles(colors, spacing, scaling), [colors, spacing, scaling]);
30
+ const { leftWidth, rightWidth, setLeftWidth, setRightWidth, centered, sideWidth } = useHeaderWithBackButton(titleAlign);
31
+
32
+ return (
33
+ <View style={[styles.container, style]}>
34
+ <View style={centered ? { width: sideWidth } : undefined}>
35
+ <View
36
+ style={styles.left}
37
+ onLayout={centered ? event => setLeftWidth(event.nativeEvent.layout.width) : undefined}
38
+ >
39
+ {!backDisabled && (
40
+ <TouchableOpacity
41
+ accessibilityRole="button"
42
+ accessibilityLabel={backAccessibilityLabel}
43
+ accessibilityHint={backAccessibilityHint}
44
+ activeOpacity={0.7}
45
+ hitSlop={styles.hitSlop}
46
+ onPress={onBackPress}
47
+ style={styles.backButton}
48
+ >
49
+ {backIcon}
50
+ </TouchableOpacity>
51
+ )}
52
+ </View>
53
+ </View>
54
+
55
+ <View
56
+ style={[
57
+ styles.content,
58
+ centered && leftWidth !== undefined && rightWidth !== undefined ? null : centered ? { opacity: 0 } : null,
59
+ contentStyle,
60
+ ]}
61
+ >
62
+ <Typography
63
+ text={title}
64
+ variant={titleVariant}
65
+ size={titleSize}
66
+ numberOfLines={1}
67
+ style={[styles.title, centered && styles.centeredText]}
68
+ />
69
+ {subtitle ? (
70
+ <Typography
71
+ text={subtitle}
72
+ variant={subtitleVariant}
73
+ size={subtitleSize}
74
+ numberOfLines={1}
75
+ style={[styles.subtitle, centered && styles.centeredText]}
76
+ />
77
+ ) : null}
78
+ </View>
79
+
80
+ <View style={centered ? { width: sideWidth } : undefined}>
81
+ <View
82
+ style={rightAccessory ? styles.right : undefined}
83
+ onLayout={centered ? event => setRightWidth(event.nativeEvent.layout.width) : undefined}
84
+ >
85
+ {rightAccessory}
86
+ </View>
87
+ </View>
88
+ </View>
89
+ );
90
+ };
@@ -0,0 +1,58 @@
1
+ import { StyleSheet } from 'react-native';
2
+ import type { UIColors, UISpacing } from '../../../theme';
3
+ import type { ScalingFunctions } from '../../../utils';
4
+
5
+ export const getStyles = (colors: UIColors, spacing: UISpacing, scaling: ScalingFunctions) => {
6
+ const backSize = scaling.scaleVertical(18);
7
+ const sideGap = scaling.scaleHorizontal(spacing.md);
8
+
9
+ return StyleSheet.create({
10
+ container: {
11
+ flexDirection: 'row',
12
+ alignItems: 'center',
13
+ paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
14
+ paddingVertical: scaling.scaleVertical(spacing.xs),
15
+ },
16
+ left: {
17
+ flexDirection: 'row',
18
+ alignItems: 'center',
19
+ alignSelf: 'flex-start',
20
+ minWidth: backSize + sideGap,
21
+ paddingRight: sideGap,
22
+ },
23
+ backButton: {
24
+ width: backSize,
25
+ height: backSize,
26
+ alignItems: 'center',
27
+ justifyContent: 'center',
28
+ },
29
+ hitSlop: {
30
+ top: scaling.scaleVertical(spacing.sm),
31
+ bottom: scaling.scaleVertical(spacing.sm),
32
+ left: scaling.scaleHorizontal(spacing.sm),
33
+ right: scaling.scaleHorizontal(spacing.sm),
34
+ },
35
+ content: {
36
+ flex: 1,
37
+ minWidth: 0,
38
+ justifyContent: 'center',
39
+ },
40
+ centeredText: {
41
+ textAlign: 'center',
42
+ },
43
+ title: {
44
+ color: colors.text,
45
+ },
46
+ subtitle: {
47
+ color: colors.textMuted,
48
+ },
49
+ right: {
50
+ minWidth: scaling.scaleVertical(34) + sideGap,
51
+ minHeight: scaling.scaleVertical(34),
52
+ alignSelf: 'flex-end',
53
+ paddingLeft: sideGap,
54
+ alignItems: 'flex-end',
55
+ justifyContent: 'center',
56
+ },
57
+ });
58
+ };
@@ -0,0 +1,8 @@
1
+ export { Input } from './ui/Input';
2
+ export type {
3
+ InputPasswordToggle,
4
+ InputPasswordToggleAction,
5
+ InputProps,
6
+ InputRef,
7
+ InputTextProps,
8
+ } from './types/types';
@@ -0,0 +1,104 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { TextInputProps } from 'react-native';
3
+ import { useUITheme } from '../../../theme';
4
+ import type { InputProps } from '../types/types';
5
+
6
+ type UseInputProps = Pick<
7
+ InputProps,
8
+ | 'accessibilityHint'
9
+ | 'accessibilityLabel'
10
+ | 'accessibilityState'
11
+ | 'disabled'
12
+ | 'editable'
13
+ | 'errorText'
14
+ | 'errorTextProps'
15
+ | 'helperText'
16
+ | 'helperTextProps'
17
+ | 'invalid'
18
+ | 'label'
19
+ | 'labelProps'
20
+ | 'onBlur'
21
+ | 'onFocus'
22
+ | 'passwordToggle'
23
+ | 'placeholderTextColor'
24
+ | 'secureTextEntry'
25
+ >;
26
+
27
+ export const useInput = ({
28
+ accessibilityHint,
29
+ accessibilityLabel,
30
+ accessibilityState,
31
+ disabled = false,
32
+ editable,
33
+ errorText,
34
+ errorTextProps,
35
+ helperText,
36
+ helperTextProps,
37
+ invalid = false,
38
+ label,
39
+ labelProps,
40
+ onBlur,
41
+ onFocus,
42
+ passwordToggle,
43
+ placeholderTextColor,
44
+ secureTextEntry,
45
+ }: UseInputProps) => {
46
+ const { colors, input } = useUITheme();
47
+ const canTogglePassword = Boolean(secureTextEntry && passwordToggle);
48
+ const [visibility, setVisibility] = useState({ enabled: canTogglePassword, visible: false });
49
+ const [isFocused, setFocused] = useState(false);
50
+
51
+ useEffect(() => {
52
+ setVisibility(current => current.enabled === canTogglePassword
53
+ ? current
54
+ : { enabled: canTogglePassword, visible: false });
55
+ }, [canTogglePassword]);
56
+
57
+ const isPasswordVisible = canTogglePassword && visibility.enabled === canTogglePassword && visibility.visible;
58
+ const isInvalid = invalid || Boolean(errorText);
59
+
60
+ const onInputFocus = useCallback<NonNullable<TextInputProps['onFocus']>>(
61
+ event => {
62
+ setFocused(true);
63
+ onFocus?.(event);
64
+ },
65
+ [onFocus],
66
+ );
67
+ const onInputBlur = useCallback<NonNullable<TextInputProps['onBlur']>>(
68
+ event => {
69
+ setFocused(false);
70
+ onBlur?.(event);
71
+ },
72
+ [onBlur],
73
+ );
74
+ const onPasswordVisibilityToggle = useCallback(() => {
75
+ setVisibility(current => ({
76
+ enabled: canTogglePassword,
77
+ visible: !(current.enabled === canTogglePassword && current.visible),
78
+ }));
79
+ }, [canTogglePassword]);
80
+ const passwordAction = isPasswordVisible ? passwordToggle?.hide : passwordToggle?.show;
81
+
82
+ return {
83
+ canTogglePassword,
84
+ errorColor: errorTextProps?.color ?? input.colors.error,
85
+ errorVariant: errorTextProps?.variant ?? input.typography.errorVariant,
86
+ helperColor: helperTextProps?.color ?? input.colors.helper,
87
+ helperVariant: helperTextProps?.variant ?? input.typography.helperVariant,
88
+ labelColor: labelProps?.color ?? input.colors.label,
89
+ labelVariant: labelProps?.variant ?? input.typography.labelVariant,
90
+ isFocused,
91
+ isInvalid,
92
+ onInputBlur,
93
+ onInputFocus,
94
+ onPasswordVisibilityToggle,
95
+ passwordAction,
96
+ resolvedAccessibilityHint: accessibilityHint ?? (errorText || undefined),
97
+ resolvedAccessibilityLabel: accessibilityLabel ?? label,
98
+ resolvedAccessibilityState: disabled ? { ...accessibilityState, disabled: true } : accessibilityState,
99
+ resolvedEditable: disabled ? false : editable,
100
+ resolvedPlaceholderTextColor: placeholderTextColor ?? colors[input.colors.placeholder],
101
+ resolvedSecureTextEntry: secureTextEntry ? !isPasswordVisible : secureTextEntry,
102
+ showHelperText: !isInvalid && Boolean(helperText),
103
+ };
104
+ };
@@ -0,0 +1,36 @@
1
+ import type { ComponentRef, ReactNode } from 'react';
2
+ import type { StyleProp, TextInput, TextInputProps, ViewStyle } from 'react-native';
3
+ import type { TypographyProps } from '../../Typography';
4
+
5
+ export type InputRef = ComponentRef<typeof TextInput>;
6
+
7
+ export type InputTextProps = Pick<
8
+ TypographyProps,
9
+ 'variant' | 'weight' | 'color' | 'style' | 'numberOfLines'
10
+ >;
11
+
12
+ export interface InputPasswordToggleAction {
13
+ label: string;
14
+ icon: ReactNode;
15
+ }
16
+
17
+ export interface InputPasswordToggle {
18
+ show: InputPasswordToggleAction;
19
+ hide: InputPasswordToggleAction;
20
+ }
21
+
22
+ export interface InputProps extends Omit<TextInputProps, 'ref'> {
23
+ label?: string;
24
+ helperText?: string;
25
+ errorText?: string;
26
+ invalid?: boolean;
27
+ disabled?: boolean;
28
+ startAccessory?: ReactNode;
29
+ endAccessory?: ReactNode;
30
+ passwordToggle?: InputPasswordToggle;
31
+ containerStyle?: StyleProp<ViewStyle>;
32
+ inputContainerStyle?: StyleProp<ViewStyle>;
33
+ labelProps?: InputTextProps;
34
+ helperTextProps?: InputTextProps;
35
+ errorTextProps?: InputTextProps;
36
+ }