@oneplatformdev/react-native-uikit 0.0.0-stage → 1.1.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.
- 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 +21 -0
- package/src/components/CustomAlert/ui/CustomAlert.tsx +79 -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 +22 -0
- package/src/components/HeaderWithBackButton/ui/HeaderWithBackButton.tsx +91 -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,23 @@
|
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { TypographyVariant } from '../../../theme';
|
|
4
|
+
|
|
5
|
+
export interface BottomModalCloseAction {
|
|
6
|
+
icon: ReactNode;
|
|
7
|
+
accessibilityLabel: string;
|
|
8
|
+
accessibilityHint?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface BottomModalProps {
|
|
12
|
+
visible: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
closeRef?: RefObject<(() => void) | null>;
|
|
16
|
+
title?: string;
|
|
17
|
+
titleVariant?: TypographyVariant;
|
|
18
|
+
customHeader?: ReactNode;
|
|
19
|
+
closeAction?: BottomModalCloseAction;
|
|
20
|
+
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
21
|
+
isFullScreen?: boolean;
|
|
22
|
+
shouldAvoidKeyboard?: boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Animated, Modal, Pressable, StyleSheet, TouchableWithoutFeedback, View } from 'react-native';
|
|
3
|
+
import Reanimated from 'react-native-reanimated';
|
|
4
|
+
import { Typography } from '../../Typography';
|
|
5
|
+
import { useUITheme } from '../../../theme';
|
|
6
|
+
import { useScaling } from '../../../utils';
|
|
7
|
+
import { useBottomModal } from '../presenters/useBottomModal';
|
|
8
|
+
import type { BottomModalProps } from '../types/types';
|
|
9
|
+
import { getStyles, useBottomModalAnimatedStyles } from './styles';
|
|
10
|
+
|
|
11
|
+
export const BottomModal = ({
|
|
12
|
+
visible,
|
|
13
|
+
onClose,
|
|
14
|
+
children,
|
|
15
|
+
closeRef,
|
|
16
|
+
title,
|
|
17
|
+
titleVariant = 'title',
|
|
18
|
+
customHeader,
|
|
19
|
+
closeAction,
|
|
20
|
+
contentContainerStyle,
|
|
21
|
+
isFullScreen = false,
|
|
22
|
+
shouldAvoidKeyboard = true,
|
|
23
|
+
}: BottomModalProps) => {
|
|
24
|
+
const { colors, spacing, radius } = useUITheme();
|
|
25
|
+
const scaling = useScaling();
|
|
26
|
+
const {
|
|
27
|
+
bottomInset,
|
|
28
|
+
height,
|
|
29
|
+
topInset,
|
|
30
|
+
translateY,
|
|
31
|
+
backdropOpacity,
|
|
32
|
+
onClosePress,
|
|
33
|
+
onShow,
|
|
34
|
+
panHandlers,
|
|
35
|
+
} = useBottomModal({ onClose, closeRef });
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
modalContentStyle,
|
|
39
|
+
animatedBackdropStyle,
|
|
40
|
+
animatedKeyboardContainerStyle,
|
|
41
|
+
animatedKeyboardBackgroundStyle,
|
|
42
|
+
} = useBottomModalAnimatedStyles(
|
|
43
|
+
height, topInset, bottomInset, translateY, backdropOpacity, isFullScreen, shouldAvoidKeyboard,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const styles = useMemo(
|
|
47
|
+
() => getStyles(colors, spacing, radius, scaling, bottomInset),
|
|
48
|
+
[bottomInset, colors, radius, scaling, spacing],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Modal visible={visible} transparent animationType="none" statusBarTranslucent onRequestClose={onClosePress} onShow={onShow}>
|
|
53
|
+
<View style={styles.modalContainer}>
|
|
54
|
+
<Animated.View pointerEvents="none" style={[styles.backdrop, animatedBackdropStyle]} />
|
|
55
|
+
<Reanimated.View pointerEvents="none" style={[styles.keyboardBackground, animatedKeyboardBackgroundStyle]} />
|
|
56
|
+
<Pressable
|
|
57
|
+
accessible={false}
|
|
58
|
+
importantForAccessibility="no"
|
|
59
|
+
onPress={onClosePress}
|
|
60
|
+
style={StyleSheet.absoluteFill}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<Reanimated.View style={animatedKeyboardContainerStyle}>
|
|
64
|
+
<Animated.View
|
|
65
|
+
accessibilityViewIsModal
|
|
66
|
+
onAccessibilityEscape={onClosePress}
|
|
67
|
+
style={[styles.modalContent, modalContentStyle]}
|
|
68
|
+
>
|
|
69
|
+
<View style={[styles.container, isFullScreen && styles.fullScreenContainer]}>
|
|
70
|
+
<View {...panHandlers}>
|
|
71
|
+
<TouchableWithoutFeedback>
|
|
72
|
+
{customHeader ? (
|
|
73
|
+
<View>{customHeader}</View>
|
|
74
|
+
) : (
|
|
75
|
+
<View style={styles.header}>
|
|
76
|
+
<View style={styles.closeButton} />
|
|
77
|
+
{title ? (
|
|
78
|
+
<View style={styles.titleContainer} pointerEvents="none">
|
|
79
|
+
<Typography text={title} variant={titleVariant} style={styles.title} />
|
|
80
|
+
</View>
|
|
81
|
+
) : null}
|
|
82
|
+
{closeAction ? (
|
|
83
|
+
<Pressable
|
|
84
|
+
accessibilityRole="button"
|
|
85
|
+
accessibilityLabel={closeAction.accessibilityLabel}
|
|
86
|
+
accessibilityHint={closeAction.accessibilityHint ?? title}
|
|
87
|
+
onPress={onClosePress}
|
|
88
|
+
style={styles.closeButton}
|
|
89
|
+
>
|
|
90
|
+
{closeAction.icon}
|
|
91
|
+
</Pressable>
|
|
92
|
+
) : <View style={styles.closeButton} />}
|
|
93
|
+
</View>
|
|
94
|
+
)}
|
|
95
|
+
</TouchableWithoutFeedback>
|
|
96
|
+
</View>
|
|
97
|
+
<View style={[
|
|
98
|
+
styles.contentContainer,
|
|
99
|
+
isFullScreen && styles.fullScreenContentContainer,
|
|
100
|
+
contentContainerStyle,
|
|
101
|
+
]}>
|
|
102
|
+
{children}
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
</Animated.View>
|
|
106
|
+
</Reanimated.View>
|
|
107
|
+
</View>
|
|
108
|
+
</Modal>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Animated, StyleSheet } from 'react-native';
|
|
3
|
+
import type { ViewStyle } from 'react-native';
|
|
4
|
+
import { useAnimatedStyle } from 'react-native-reanimated';
|
|
5
|
+
import { useAnimatedKeyboard } from 'react-native-keyboard-controller';
|
|
6
|
+
import type { UIColors, UIRadius, UISpacing } from '../../../theme';
|
|
7
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
8
|
+
|
|
9
|
+
export const getStyles = (
|
|
10
|
+
colors: UIColors,
|
|
11
|
+
spacing: UISpacing,
|
|
12
|
+
radius: UIRadius,
|
|
13
|
+
scaling: ScalingFunctions,
|
|
14
|
+
bottomInset: number,
|
|
15
|
+
) => {
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
modalContainer: {
|
|
18
|
+
flex: 1,
|
|
19
|
+
justifyContent: 'flex-end',
|
|
20
|
+
},
|
|
21
|
+
backdrop: {
|
|
22
|
+
...StyleSheet.absoluteFill,
|
|
23
|
+
backgroundColor: colors.text,
|
|
24
|
+
},
|
|
25
|
+
keyboardBackground: {
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
left: 0,
|
|
28
|
+
right: 0,
|
|
29
|
+
bottom: 0,
|
|
30
|
+
backgroundColor: colors.background,
|
|
31
|
+
},
|
|
32
|
+
modalContent: {
|
|
33
|
+
backgroundColor: colors.background,
|
|
34
|
+
borderTopLeftRadius: scaling.scaleHorizontal(radius.lg),
|
|
35
|
+
borderTopRightRadius: scaling.scaleHorizontal(radius.lg),
|
|
36
|
+
overflow: 'hidden',
|
|
37
|
+
flexShrink: 1,
|
|
38
|
+
},
|
|
39
|
+
container: {
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
flexShrink: 1,
|
|
42
|
+
},
|
|
43
|
+
fullScreenContainer: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
minHeight: 0,
|
|
46
|
+
},
|
|
47
|
+
header: {
|
|
48
|
+
flexDirection: 'row',
|
|
49
|
+
alignItems: 'center',
|
|
50
|
+
justifyContent: 'flex-end',
|
|
51
|
+
paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
|
|
52
|
+
paddingTop: scaling.scaleVertical(spacing.lg),
|
|
53
|
+
position: 'relative',
|
|
54
|
+
},
|
|
55
|
+
titleContainer: {
|
|
56
|
+
flex: 1,
|
|
57
|
+
flexShrink: 1,
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
justifyContent: 'center',
|
|
60
|
+
paddingVertical: scaling.scaleVertical(spacing.sm),
|
|
61
|
+
},
|
|
62
|
+
closeButton: {
|
|
63
|
+
width: scaling.scaleHorizontal(40),
|
|
64
|
+
height: scaling.scaleHorizontal(40),
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
},
|
|
68
|
+
title: {
|
|
69
|
+
textAlign: 'center',
|
|
70
|
+
color: colors.text,
|
|
71
|
+
},
|
|
72
|
+
contentContainer: {
|
|
73
|
+
paddingHorizontal: scaling.scaleHorizontal(spacing.lg),
|
|
74
|
+
paddingBottom: bottomInset + scaling.scaleVertical(spacing.lg),
|
|
75
|
+
flexShrink: 1,
|
|
76
|
+
},
|
|
77
|
+
fullScreenContentContainer: {
|
|
78
|
+
flex: 1,
|
|
79
|
+
minHeight: 0,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return styles;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const useBottomModalAnimatedStyles = (
|
|
87
|
+
height: number,
|
|
88
|
+
topInset: number,
|
|
89
|
+
bottomInset: number,
|
|
90
|
+
translateY: Animated.Value,
|
|
91
|
+
backdropOpacity: Animated.Value,
|
|
92
|
+
isFullScreen: boolean,
|
|
93
|
+
shouldAvoidKeyboard: boolean,
|
|
94
|
+
) => {
|
|
95
|
+
const maxHeight = height - topInset;
|
|
96
|
+
const { height: keyboardHeight } = useAnimatedKeyboard();
|
|
97
|
+
const animatedBackdropStyle = useMemo(
|
|
98
|
+
() => ({ opacity: backdropOpacity.interpolate({ inputRange: [0, 1], outputRange: [0, 0.5] }) }),
|
|
99
|
+
[backdropOpacity],
|
|
100
|
+
);
|
|
101
|
+
const animatedKeyboardContainerStyle = useAnimatedStyle(() => {
|
|
102
|
+
if (!shouldAvoidKeyboard) return { marginBottom: 0, maxHeight };
|
|
103
|
+
const keyboardOffset = Math.max(keyboardHeight.value - bottomInset, 0);
|
|
104
|
+
return { marginBottom: keyboardOffset, maxHeight: Math.max(maxHeight - keyboardOffset, 0) };
|
|
105
|
+
});
|
|
106
|
+
const animatedKeyboardBackgroundStyle = useAnimatedStyle(() => ({
|
|
107
|
+
height: shouldAvoidKeyboard ? Math.max(keyboardHeight.value - bottomInset, 0) : 0,
|
|
108
|
+
}));
|
|
109
|
+
const modalContentStyle = useMemo<ViewStyle>(
|
|
110
|
+
() => ({ maxHeight, height: isFullScreen ? maxHeight : undefined, transform: [{ translateY }] }),
|
|
111
|
+
[isFullScreen, maxHeight, translateY],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return { animatedBackdropStyle, animatedKeyboardContainerStyle, animatedKeyboardBackgroundStyle, modalContentStyle };
|
|
115
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useUITheme } from "../../../theme";
|
|
2
|
+
import type { ButtonProps } from "../types/types";
|
|
3
|
+
|
|
4
|
+
type UseButtonProps = Pick<
|
|
5
|
+
ButtonProps,
|
|
6
|
+
| "accessibilityLabel"
|
|
7
|
+
| "accessibilityRole"
|
|
8
|
+
| "accessibilityState"
|
|
9
|
+
| "children"
|
|
10
|
+
| "text"
|
|
11
|
+
| "disabled"
|
|
12
|
+
| "labelProps"
|
|
13
|
+
| "loading"
|
|
14
|
+
| "loadingAppearance"
|
|
15
|
+
| "loadingIndicatorColor"
|
|
16
|
+
| "size"
|
|
17
|
+
| "variant"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export const useButton = ({
|
|
21
|
+
accessibilityLabel,
|
|
22
|
+
accessibilityRole,
|
|
23
|
+
accessibilityState,
|
|
24
|
+
children,
|
|
25
|
+
text,
|
|
26
|
+
disabled,
|
|
27
|
+
labelProps,
|
|
28
|
+
loading,
|
|
29
|
+
loadingAppearance,
|
|
30
|
+
loadingIndicatorColor,
|
|
31
|
+
size,
|
|
32
|
+
variant,
|
|
33
|
+
}: UseButtonProps) => {
|
|
34
|
+
const { button, colors } = useUITheme();
|
|
35
|
+
const variantTheme = button.variants[variant ?? "primary"];
|
|
36
|
+
const sizeTheme = button.sizes[size ?? "md"];
|
|
37
|
+
const interactionDisabled = Boolean(disabled || loading);
|
|
38
|
+
const visuallyDisabled = Boolean(
|
|
39
|
+
disabled || (loading && loadingAppearance === "disabled")
|
|
40
|
+
);
|
|
41
|
+
const textColor = visuallyDisabled
|
|
42
|
+
? variantTheme.disabledForeground
|
|
43
|
+
: variantTheme.foreground;
|
|
44
|
+
const content = text ?? children;
|
|
45
|
+
const isTextChild =
|
|
46
|
+
typeof content === "string" || typeof content === "number";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content,
|
|
50
|
+
colors,
|
|
51
|
+
interactionDisabled,
|
|
52
|
+
isTextChild,
|
|
53
|
+
labelColor: labelProps?.color ?? textColor,
|
|
54
|
+
labelNumberOfLines: labelProps?.numberOfLines ?? button.numberOfLines,
|
|
55
|
+
labelVariant: labelProps?.variant ?? sizeTheme.labelVariant,
|
|
56
|
+
labelWeight: labelProps?.weight ?? sizeTheme.labelWeight,
|
|
57
|
+
sizeTheme,
|
|
58
|
+
variantTheme,
|
|
59
|
+
visuallyDisabled,
|
|
60
|
+
loaderColor: loading
|
|
61
|
+
? loadingIndicatorColor ?? colors[textColor]
|
|
62
|
+
: undefined,
|
|
63
|
+
resolvedAccessibilityLabel:
|
|
64
|
+
accessibilityLabel ?? (isTextChild ? String(content) : undefined),
|
|
65
|
+
resolvedAccessibilityRole: accessibilityRole ?? "button",
|
|
66
|
+
resolvedAccessibilityState: {
|
|
67
|
+
...accessibilityState,
|
|
68
|
+
disabled: interactionDisabled,
|
|
69
|
+
busy: loading,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { PressableProps } from 'react-native';
|
|
3
|
+
import type { TypographyProps } from '../../Typography';
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
6
|
+
|
|
7
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
8
|
+
|
|
9
|
+
export type ButtonLoadingAppearance = 'active' | 'disabled';
|
|
10
|
+
|
|
11
|
+
export type ButtonLabelProps = Omit<
|
|
12
|
+
TypographyProps,
|
|
13
|
+
| 'children'
|
|
14
|
+
| 'text'
|
|
15
|
+
| 'onPress'
|
|
16
|
+
| 'onLongPress'
|
|
17
|
+
| 'onPressIn'
|
|
18
|
+
| 'onPressOut'
|
|
19
|
+
| 'accessible'
|
|
20
|
+
| 'accessibilityLabel'
|
|
21
|
+
| 'accessibilityHint'
|
|
22
|
+
| 'accessibilityRole'
|
|
23
|
+
| 'accessibilityState'
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export type ButtonProps = Omit<PressableProps, 'children'> & {
|
|
27
|
+
variant?: ButtonVariant;
|
|
28
|
+
size?: ButtonSize;
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
loadingAppearance?: ButtonLoadingAppearance;
|
|
31
|
+
loadingIndicatorColor?: string;
|
|
32
|
+
leftIcon?: ReactNode;
|
|
33
|
+
rightIcon?: ReactNode;
|
|
34
|
+
fullWidth?: boolean;
|
|
35
|
+
labelProps?: ButtonLabelProps;
|
|
36
|
+
} & (
|
|
37
|
+
| { text: string | number; children?: never }
|
|
38
|
+
| { text?: never; children?: ReactNode }
|
|
39
|
+
);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { forwardRef, memo, useCallback, useMemo } from "react";
|
|
2
|
+
import { ActivityIndicator, Pressable, View } from "react-native";
|
|
3
|
+
import type { PressableInstance, PressableStateCallbackType } from "react-native";
|
|
4
|
+
import { Typography } from "../../Typography";
|
|
5
|
+
import { useButton } from "../presenters/useButton";
|
|
6
|
+
import type { ButtonProps } from "../types/types";
|
|
7
|
+
import { useScaling } from '../../../utils';
|
|
8
|
+
import { getStyles, resolveButtonStyle } from './styles';
|
|
9
|
+
|
|
10
|
+
const ButtonComponent = forwardRef<PressableInstance, ButtonProps>(
|
|
11
|
+
(
|
|
12
|
+
{
|
|
13
|
+
accessibilityLabel,
|
|
14
|
+
accessibilityRole,
|
|
15
|
+
accessibilityState,
|
|
16
|
+
children,
|
|
17
|
+
text,
|
|
18
|
+
disabled = false,
|
|
19
|
+
fullWidth = false,
|
|
20
|
+
labelProps,
|
|
21
|
+
leftIcon,
|
|
22
|
+
loading = false,
|
|
23
|
+
loadingAppearance = "active",
|
|
24
|
+
loadingIndicatorColor,
|
|
25
|
+
rightIcon,
|
|
26
|
+
size = "md",
|
|
27
|
+
style,
|
|
28
|
+
variant = "primary",
|
|
29
|
+
...props
|
|
30
|
+
},
|
|
31
|
+
ref
|
|
32
|
+
) => {
|
|
33
|
+
const {
|
|
34
|
+
content,
|
|
35
|
+
colors,
|
|
36
|
+
interactionDisabled,
|
|
37
|
+
isTextChild,
|
|
38
|
+
labelColor,
|
|
39
|
+
labelNumberOfLines,
|
|
40
|
+
labelVariant,
|
|
41
|
+
labelWeight,
|
|
42
|
+
loaderColor,
|
|
43
|
+
resolvedAccessibilityLabel,
|
|
44
|
+
resolvedAccessibilityRole,
|
|
45
|
+
resolvedAccessibilityState,
|
|
46
|
+
sizeTheme,
|
|
47
|
+
variantTheme,
|
|
48
|
+
visuallyDisabled,
|
|
49
|
+
} = useButton({
|
|
50
|
+
accessibilityLabel,
|
|
51
|
+
accessibilityRole,
|
|
52
|
+
accessibilityState,
|
|
53
|
+
children,
|
|
54
|
+
text,
|
|
55
|
+
disabled,
|
|
56
|
+
labelProps,
|
|
57
|
+
loading,
|
|
58
|
+
loadingAppearance,
|
|
59
|
+
loadingIndicatorColor,
|
|
60
|
+
size,
|
|
61
|
+
variant,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const scaling = useScaling();
|
|
65
|
+
const styles = useMemo(
|
|
66
|
+
() => getStyles(colors, scaling, variantTheme, sizeTheme),
|
|
67
|
+
[colors, scaling, variantTheme, sizeTheme]
|
|
68
|
+
);
|
|
69
|
+
const resolveStyle = useCallback(
|
|
70
|
+
({ pressed }: PressableStateCallbackType) => resolveButtonStyle(
|
|
71
|
+
styles, pressed, style, interactionDisabled, visuallyDisabled, fullWidth
|
|
72
|
+
),
|
|
73
|
+
[styles, style, interactionDisabled, visuallyDisabled, fullWidth]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Pressable
|
|
78
|
+
{...props}
|
|
79
|
+
ref={ref}
|
|
80
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
81
|
+
accessibilityRole={resolvedAccessibilityRole}
|
|
82
|
+
accessibilityState={resolvedAccessibilityState}
|
|
83
|
+
disabled={interactionDisabled}
|
|
84
|
+
style={resolveStyle}
|
|
85
|
+
>
|
|
86
|
+
<View
|
|
87
|
+
accessibilityElementsHidden={loading}
|
|
88
|
+
importantForAccessibility={loading ? "no-hide-descendants" : "auto"}
|
|
89
|
+
style={[styles.content, loading && styles.hidden]}
|
|
90
|
+
>
|
|
91
|
+
{leftIcon ? <View style={styles.icon}>{leftIcon}</View> : null}
|
|
92
|
+
{isTextChild ? (
|
|
93
|
+
<Typography
|
|
94
|
+
{...labelProps}
|
|
95
|
+
numberOfLines={labelNumberOfLines}
|
|
96
|
+
color={labelColor}
|
|
97
|
+
variant={labelVariant}
|
|
98
|
+
weight={labelWeight}
|
|
99
|
+
style={[styles.text, labelProps?.style]}
|
|
100
|
+
>
|
|
101
|
+
{content}
|
|
102
|
+
</Typography>
|
|
103
|
+
) : (
|
|
104
|
+
content
|
|
105
|
+
)}
|
|
106
|
+
{rightIcon ? <View style={styles.icon}>{rightIcon}</View> : null}
|
|
107
|
+
</View>
|
|
108
|
+
|
|
109
|
+
{loading ? (
|
|
110
|
+
<View pointerEvents="none" style={styles.loader}>
|
|
111
|
+
<ActivityIndicator color={loaderColor} size="small" />
|
|
112
|
+
</View>
|
|
113
|
+
) : null}
|
|
114
|
+
</Pressable>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
ButtonComponent.displayName = "Button";
|
|
120
|
+
|
|
121
|
+
export const Button = memo(ButtonComponent);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
import type { UIColors, UIButtonSizeTheme, UIButtonVariantTheme } from '../../../theme';
|
|
4
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
5
|
+
import type { ButtonProps } from '../types/types';
|
|
6
|
+
|
|
7
|
+
export const getStyles = (
|
|
8
|
+
colors: UIColors,
|
|
9
|
+
scaling: ScalingFunctions,
|
|
10
|
+
variant: UIButtonVariantTheme,
|
|
11
|
+
size: UIButtonSizeTheme,
|
|
12
|
+
) => {
|
|
13
|
+
const container: ViewStyle = {
|
|
14
|
+
minHeight: scaling.scaleVertical(size.minHeight),
|
|
15
|
+
paddingHorizontal: scaling.scaleHorizontal(size.paddingHorizontal),
|
|
16
|
+
paddingVertical: scaling.scaleVertical(size.paddingVertical),
|
|
17
|
+
borderRadius: scaling.scaleHorizontal(size.radius),
|
|
18
|
+
borderWidth: scaling.scaleHorizontal(variant.borderWidth ?? size.borderWidth),
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
backgroundColor: colors[variant.background],
|
|
22
|
+
borderColor: colors[variant.border],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const text: TextStyle = {
|
|
26
|
+
flexShrink: 1,
|
|
27
|
+
minWidth: 0,
|
|
28
|
+
textAlign: 'center',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
container,
|
|
33
|
+
content: {
|
|
34
|
+
width: '100%',
|
|
35
|
+
flexDirection: 'row',
|
|
36
|
+
alignItems: 'center',
|
|
37
|
+
justifyContent: 'center',
|
|
38
|
+
gap: scaling.scaleHorizontal(size.gap),
|
|
39
|
+
},
|
|
40
|
+
disabled: {
|
|
41
|
+
backgroundColor: colors[variant.disabledBackground],
|
|
42
|
+
borderColor: colors[variant.disabledBorder],
|
|
43
|
+
},
|
|
44
|
+
fullWidth: {
|
|
45
|
+
width: '100%',
|
|
46
|
+
},
|
|
47
|
+
hidden: {
|
|
48
|
+
opacity: 0,
|
|
49
|
+
},
|
|
50
|
+
icon: {
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
justifyContent: 'center',
|
|
54
|
+
},
|
|
55
|
+
loader: {
|
|
56
|
+
...StyleSheet.absoluteFill,
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
},
|
|
60
|
+
pressed: {
|
|
61
|
+
opacity: 0.82,
|
|
62
|
+
},
|
|
63
|
+
text,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return styles;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const resolveButtonStyle = (
|
|
70
|
+
styles: ReturnType<typeof getStyles>,
|
|
71
|
+
pressed: boolean,
|
|
72
|
+
style: ButtonProps['style'],
|
|
73
|
+
interactionDisabled: boolean,
|
|
74
|
+
visuallyDisabled: boolean,
|
|
75
|
+
fullWidth: boolean,
|
|
76
|
+
): StyleProp<ViewStyle> => [
|
|
77
|
+
styles.container,
|
|
78
|
+
pressed && !interactionDisabled && styles.pressed,
|
|
79
|
+
visuallyDisabled && styles.disabled,
|
|
80
|
+
fullWidth && styles.fullWidth,
|
|
81
|
+
typeof style === 'function' ? style({ pressed }) : style,
|
|
82
|
+
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { ConnectionContainerProps } from '../types/types';
|
|
3
|
+
|
|
4
|
+
type ConnectionStatus = 'disconnected' | 'reconnected' | null;
|
|
5
|
+
|
|
6
|
+
const DISCONNECT_DELAY = 3000;
|
|
7
|
+
const RECONNECT_VISIBLE_TIME = 3000;
|
|
8
|
+
|
|
9
|
+
export const useConnectionContainer = ({
|
|
10
|
+
isConnected,
|
|
11
|
+
disconnectedText,
|
|
12
|
+
reconnectedText,
|
|
13
|
+
}: Pick<ConnectionContainerProps, 'isConnected' | 'disconnectedText' | 'reconnectedText'>) => {
|
|
14
|
+
const [status, setStatus] = useState<ConnectionStatus>(null);
|
|
15
|
+
const initializedRef = useRef(false);
|
|
16
|
+
const disconnectedBannerShownRef = useRef(false);
|
|
17
|
+
const disconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
18
|
+
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
19
|
+
|
|
20
|
+
const clearDisconnectTimeout = useCallback(() => {
|
|
21
|
+
if (disconnectTimeoutRef.current !== null) {
|
|
22
|
+
clearTimeout(disconnectTimeoutRef.current);
|
|
23
|
+
disconnectTimeoutRef.current = null;
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
const clearHideTimeout = useCallback(() => {
|
|
27
|
+
if (hideTimeoutRef.current !== null) {
|
|
28
|
+
clearTimeout(hideTimeoutRef.current);
|
|
29
|
+
hideTimeoutRef.current = null;
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
const onHideBanner = useCallback(() => {
|
|
33
|
+
clearHideTimeout();
|
|
34
|
+
setStatus(null);
|
|
35
|
+
}, [clearHideTimeout]);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (isConnected === null) return;
|
|
39
|
+
if (!initializedRef.current) {
|
|
40
|
+
initializedRef.current = true;
|
|
41
|
+
if (isConnected) return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!isConnected) {
|
|
45
|
+
if (disconnectTimeoutRef.current !== null || disconnectedBannerShownRef.current) return;
|
|
46
|
+
clearHideTimeout();
|
|
47
|
+
disconnectTimeoutRef.current = setTimeout(() => {
|
|
48
|
+
disconnectTimeoutRef.current = null;
|
|
49
|
+
disconnectedBannerShownRef.current = true;
|
|
50
|
+
setStatus('disconnected');
|
|
51
|
+
}, DISCONNECT_DELAY);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clearDisconnectTimeout();
|
|
56
|
+
if (!disconnectedBannerShownRef.current) return;
|
|
57
|
+
disconnectedBannerShownRef.current = false;
|
|
58
|
+
clearHideTimeout();
|
|
59
|
+
setStatus('reconnected');
|
|
60
|
+
hideTimeoutRef.current = setTimeout(() => {
|
|
61
|
+
hideTimeoutRef.current = null;
|
|
62
|
+
setStatus(null);
|
|
63
|
+
}, RECONNECT_VISIBLE_TIME);
|
|
64
|
+
}, [isConnected, clearDisconnectTimeout, clearHideTimeout]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => () => {
|
|
67
|
+
clearDisconnectTimeout();
|
|
68
|
+
clearHideTimeout();
|
|
69
|
+
}, [clearDisconnectTimeout, clearHideTimeout]);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
visible: status !== null,
|
|
73
|
+
isReconnected: status === 'reconnected',
|
|
74
|
+
text: status === 'reconnected' ? reconnectedText : disconnectedText,
|
|
75
|
+
onHideBanner,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface ConnectionContainerProps {
|
|
5
|
+
/** null means connectivity is not yet known. */
|
|
6
|
+
isConnected: boolean | null;
|
|
7
|
+
disconnectedText: string;
|
|
8
|
+
reconnectedText: string;
|
|
9
|
+
children?: ReactNode;
|
|
10
|
+
disconnectedIcon?: ReactNode;
|
|
11
|
+
reconnectedIcon?: ReactNode;
|
|
12
|
+
dismissAccessibilityHint?: string;
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
}
|