@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,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,8 @@
1
+ export { Button } from './ui/Button';
2
+ export type {
3
+ ButtonLabelProps,
4
+ ButtonLoadingAppearance,
5
+ ButtonProps,
6
+ ButtonSize,
7
+ ButtonVariant,
8
+ } from './types/types';
@@ -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,2 @@
1
+ export { ConnectionContainer } from './ui/ConnectionContainer';
2
+ export type { ConnectionContainerProps } from './types/types';
@@ -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
+ }