@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.
- package/README.md +376 -2
- package/bin/cli.cjs +372 -0
- package/package.json +34 -4
- package/registry/manifest.json +113 -0
- package/src/components/BottomModal/index.ts +2 -0
- package/src/components/BottomModal/presenters/useBottomModal.ts +114 -0
- package/src/components/BottomModal/presenters/useBottomModalInsets.ts +19 -0
- package/src/components/BottomModal/types/types.ts +23 -0
- package/src/components/BottomModal/ui/BottomModal.tsx +110 -0
- package/src/components/BottomModal/ui/styles.ts +115 -0
- package/src/components/Button/index.ts +8 -0
- package/src/components/Button/presenters/useButton.ts +72 -0
- package/src/components/Button/types/types.ts +39 -0
- package/src/components/Button/ui/Button.tsx +121 -0
- package/src/components/Button/ui/styles.ts +82 -0
- package/src/components/ConnectionContainer/index.ts +2 -0
- package/src/components/ConnectionContainer/presenters/useConnectionContainer.ts +77 -0
- package/src/components/ConnectionContainer/types/types.ts +14 -0
- package/src/components/ConnectionContainer/ui/ConnectionContainer.tsx +51 -0
- package/src/components/ConnectionContainer/ui/styles.ts +35 -0
- package/src/components/CustomAlert/index.ts +2 -0
- package/src/components/CustomAlert/presenters/useCustomAlert.ts +24 -0
- package/src/components/CustomAlert/types/types.ts +20 -0
- package/src/components/CustomAlert/ui/CustomAlert.tsx +77 -0
- package/src/components/CustomAlert/ui/styles.ts +71 -0
- package/src/components/HeaderWithBackButton/index.ts +2 -0
- package/src/components/HeaderWithBackButton/presenters/useHeaderWithBackButton.ts +17 -0
- package/src/components/HeaderWithBackButton/types/types.ts +21 -0
- package/src/components/HeaderWithBackButton/ui/HeaderWithBackButton.tsx +90 -0
- package/src/components/HeaderWithBackButton/ui/styles.ts +58 -0
- package/src/components/Input/index.ts +8 -0
- package/src/components/Input/presenters/useInput.ts +104 -0
- package/src/components/Input/types/types.ts +36 -0
- package/src/components/Input/ui/Input.tsx +161 -0
- package/src/components/Input/ui/styles.ts +80 -0
- package/src/components/Loader/index.ts +2 -0
- package/src/components/Loader/types/types.ts +12 -0
- package/src/components/Loader/ui/Loader.tsx +29 -0
- package/src/components/Loader/ui/styles.ts +14 -0
- package/src/components/Logger/index.ts +2 -0
- package/src/components/Logger/presenters/useLogger.ts +13 -0
- package/src/components/Logger/types/types.ts +56 -0
- package/src/components/Logger/ui/Logger.tsx +85 -0
- package/src/components/Logger/ui/components/LoggerItem.tsx +117 -0
- package/src/components/Logger/ui/styles.ts +121 -0
- package/src/components/Logger/utils/groupLoggerEntries.ts +40 -0
- package/src/components/Logger/utils/loggerPresentation.ts +69 -0
- package/src/components/ScreenContainer/index.ts +2 -0
- package/src/components/ScreenContainer/presenters/useScreenContainer.ts +12 -0
- package/src/components/ScreenContainer/types/types.ts +19 -0
- package/src/components/ScreenContainer/ui/ScreenContainer.tsx +83 -0
- package/src/components/ScreenContainer/ui/styles.ts +31 -0
- package/src/components/Typography/index.ts +2 -0
- package/src/components/Typography/types/types.ts +14 -0
- package/src/components/Typography/ui/Typography.tsx +37 -0
- package/src/components/Typography/ui/styles.ts +26 -0
- package/src/index.ts +13 -0
- package/src/provider/LocalizationContext.tsx +14 -0
- package/src/provider/ModeContext.tsx +9 -0
- package/src/provider/index.ts +12 -0
- package/src/provider/types.ts +31 -0
- package/src/theme/ThemeProvider.tsx +7 -0
- package/src/theme/UIProvider.tsx +60 -0
- package/src/theme/createTheme.ts +66 -0
- package/src/theme/defaultTheme.ts +215 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/types.ts +265 -0
- 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,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,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,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
|
+
}
|