@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,161 @@
|
|
|
1
|
+
import React, { forwardRef, memo, useMemo } from 'react';
|
|
2
|
+
import { Pressable, TextInput, View } from 'react-native';
|
|
3
|
+
import { Typography } from '../../Typography';
|
|
4
|
+
import { useUITheme } from '../../../theme';
|
|
5
|
+
import { useScaling } from '../../../utils';
|
|
6
|
+
import { useInput } from '../presenters/useInput';
|
|
7
|
+
import type { InputProps, InputRef } from '../types/types';
|
|
8
|
+
import { getPasswordHitSlop, getStyles } from './styles';
|
|
9
|
+
|
|
10
|
+
const InputComponent = forwardRef<InputRef, InputProps>(
|
|
11
|
+
(
|
|
12
|
+
{
|
|
13
|
+
accessibilityHint,
|
|
14
|
+
accessibilityLabel,
|
|
15
|
+
accessibilityState,
|
|
16
|
+
containerStyle,
|
|
17
|
+
disabled = false,
|
|
18
|
+
editable,
|
|
19
|
+
endAccessory,
|
|
20
|
+
errorText,
|
|
21
|
+
errorTextProps,
|
|
22
|
+
helperText,
|
|
23
|
+
helperTextProps,
|
|
24
|
+
inputContainerStyle,
|
|
25
|
+
invalid = false,
|
|
26
|
+
label,
|
|
27
|
+
labelProps,
|
|
28
|
+
multiline,
|
|
29
|
+
onBlur,
|
|
30
|
+
onFocus,
|
|
31
|
+
passwordToggle,
|
|
32
|
+
placeholderTextColor,
|
|
33
|
+
secureTextEntry,
|
|
34
|
+
startAccessory,
|
|
35
|
+
style,
|
|
36
|
+
...nativeProps
|
|
37
|
+
},
|
|
38
|
+
ref,
|
|
39
|
+
) => {
|
|
40
|
+
const {
|
|
41
|
+
canTogglePassword,
|
|
42
|
+
errorColor,
|
|
43
|
+
errorVariant,
|
|
44
|
+
helperColor,
|
|
45
|
+
helperVariant,
|
|
46
|
+
isFocused,
|
|
47
|
+
isInvalid,
|
|
48
|
+
labelColor,
|
|
49
|
+
labelVariant,
|
|
50
|
+
onInputBlur,
|
|
51
|
+
onInputFocus,
|
|
52
|
+
onPasswordVisibilityToggle,
|
|
53
|
+
passwordAction,
|
|
54
|
+
resolvedAccessibilityHint,
|
|
55
|
+
resolvedAccessibilityLabel,
|
|
56
|
+
resolvedAccessibilityState,
|
|
57
|
+
resolvedEditable,
|
|
58
|
+
resolvedPlaceholderTextColor,
|
|
59
|
+
resolvedSecureTextEntry,
|
|
60
|
+
showHelperText,
|
|
61
|
+
} = useInput({
|
|
62
|
+
accessibilityHint,
|
|
63
|
+
accessibilityLabel,
|
|
64
|
+
accessibilityState,
|
|
65
|
+
disabled,
|
|
66
|
+
editable,
|
|
67
|
+
errorText,
|
|
68
|
+
errorTextProps,
|
|
69
|
+
helperText,
|
|
70
|
+
helperTextProps,
|
|
71
|
+
invalid,
|
|
72
|
+
label,
|
|
73
|
+
labelProps,
|
|
74
|
+
onBlur,
|
|
75
|
+
onFocus,
|
|
76
|
+
passwordToggle,
|
|
77
|
+
placeholderTextColor,
|
|
78
|
+
secureTextEntry,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const { colors, fonts, input } = useUITheme();
|
|
82
|
+
const scaling = useScaling();
|
|
83
|
+
const styles = useMemo(
|
|
84
|
+
() => getStyles(colors, fonts, input, scaling, isFocused, isInvalid, disabled),
|
|
85
|
+
[colors, disabled, fonts, input, isFocused, isInvalid, scaling],
|
|
86
|
+
);
|
|
87
|
+
const textInputStyle = [styles.text, multiline && styles.multilineText, style];
|
|
88
|
+
const passwordHitSlop = canTogglePassword && passwordAction ? getPasswordHitSlop(scaling) : undefined;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View style={[styles.container, containerStyle]}>
|
|
92
|
+
{label ? (
|
|
93
|
+
<View style={styles.labelContainer}>
|
|
94
|
+
<Typography
|
|
95
|
+
{...labelProps}
|
|
96
|
+
text={label}
|
|
97
|
+
variant={labelVariant}
|
|
98
|
+
color={labelColor}
|
|
99
|
+
style={labelProps?.style}
|
|
100
|
+
/>
|
|
101
|
+
</View>
|
|
102
|
+
) : null}
|
|
103
|
+
|
|
104
|
+
<View style={[styles.field, inputContainerStyle]}>
|
|
105
|
+
{startAccessory != null ? <View style={styles.startAccessory}>{startAccessory}</View> : null}
|
|
106
|
+
<TextInput
|
|
107
|
+
{...nativeProps}
|
|
108
|
+
ref={ref}
|
|
109
|
+
accessibilityLabel={resolvedAccessibilityLabel}
|
|
110
|
+
accessibilityHint={resolvedAccessibilityHint}
|
|
111
|
+
accessibilityState={resolvedAccessibilityState}
|
|
112
|
+
editable={resolvedEditable}
|
|
113
|
+
multiline={multiline}
|
|
114
|
+
placeholderTextColor={resolvedPlaceholderTextColor}
|
|
115
|
+
secureTextEntry={resolvedSecureTextEntry}
|
|
116
|
+
onFocus={onInputFocus}
|
|
117
|
+
onBlur={onInputBlur}
|
|
118
|
+
style={textInputStyle}
|
|
119
|
+
/>
|
|
120
|
+
{endAccessory != null ? <View style={styles.endAccessory}>{endAccessory}</View> : null}
|
|
121
|
+
{canTogglePassword && passwordAction ? (
|
|
122
|
+
<Pressable
|
|
123
|
+
accessibilityRole="button"
|
|
124
|
+
accessibilityLabel={passwordAction.label}
|
|
125
|
+
disabled={disabled}
|
|
126
|
+
hitSlop={passwordHitSlop}
|
|
127
|
+
onPress={onPasswordVisibilityToggle}
|
|
128
|
+
style={styles.passwordToggle}
|
|
129
|
+
>
|
|
130
|
+
{passwordAction.icon}
|
|
131
|
+
</Pressable>
|
|
132
|
+
) : null}
|
|
133
|
+
</View>
|
|
134
|
+
|
|
135
|
+
{errorText ? (
|
|
136
|
+
<Typography
|
|
137
|
+
{...errorTextProps}
|
|
138
|
+
text={errorText}
|
|
139
|
+
variant={errorVariant}
|
|
140
|
+
color={errorColor}
|
|
141
|
+
accessibilityRole="alert"
|
|
142
|
+
accessibilityLiveRegion="polite"
|
|
143
|
+
style={[styles.supportingText, errorTextProps?.style]}
|
|
144
|
+
/>
|
|
145
|
+
) : showHelperText && helperText ? (
|
|
146
|
+
<Typography
|
|
147
|
+
{...helperTextProps}
|
|
148
|
+
text={helperText}
|
|
149
|
+
variant={helperVariant}
|
|
150
|
+
color={helperColor}
|
|
151
|
+
style={[styles.supportingText, helperTextProps?.style]}
|
|
152
|
+
/>
|
|
153
|
+
) : null}
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
InputComponent.displayName = 'Input';
|
|
160
|
+
|
|
161
|
+
export const Input = memo(InputComponent);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { TextStyle, ViewStyle } from 'react-native';
|
|
3
|
+
import type { UIColors, UIFonts, UIInputTheme } from '../../../theme';
|
|
4
|
+
import type { ScalingFunctions } from '../../../utils';
|
|
5
|
+
|
|
6
|
+
export const getStyles = (
|
|
7
|
+
colors: UIColors,
|
|
8
|
+
fonts: UIFonts,
|
|
9
|
+
input: UIInputTheme,
|
|
10
|
+
scaling: ScalingFunctions,
|
|
11
|
+
focused: boolean,
|
|
12
|
+
invalid: boolean,
|
|
13
|
+
disabled: boolean,
|
|
14
|
+
) => {
|
|
15
|
+
const { geometry, typography, colors: tokens } = input;
|
|
16
|
+
const border = disabled
|
|
17
|
+
? tokens.disabledBorder
|
|
18
|
+
: invalid
|
|
19
|
+
? tokens.errorBorder
|
|
20
|
+
: focused
|
|
21
|
+
? tokens.focusedBorder
|
|
22
|
+
: tokens.border;
|
|
23
|
+
const field: ViewStyle = {
|
|
24
|
+
minHeight: scaling.scaleVertical(geometry.minHeight),
|
|
25
|
+
flexDirection: 'row',
|
|
26
|
+
alignItems: 'center',
|
|
27
|
+
paddingHorizontal: scaling.scaleHorizontal(geometry.paddingHorizontal),
|
|
28
|
+
borderWidth: scaling.scaleHorizontal(geometry.borderWidth),
|
|
29
|
+
borderColor: colors[border],
|
|
30
|
+
borderRadius: scaling.scaleHorizontal(geometry.radius),
|
|
31
|
+
backgroundColor: colors[disabled ? tokens.disabledBackground : tokens.background],
|
|
32
|
+
};
|
|
33
|
+
const text: TextStyle = {
|
|
34
|
+
flex: 1,
|
|
35
|
+
minWidth: 0,
|
|
36
|
+
minHeight: scaling.scaleVertical(geometry.textMinHeight),
|
|
37
|
+
paddingVertical: 0,
|
|
38
|
+
...fonts[typography.inputFont],
|
|
39
|
+
fontSize: scaling.scaleFontSize(typography.inputFontSize),
|
|
40
|
+
includeFontPadding: typography.includeFontPadding,
|
|
41
|
+
color: colors[disabled ? tokens.disabledText : tokens.text],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const styles = StyleSheet.create({
|
|
45
|
+
container: {
|
|
46
|
+
marginBottom: scaling.scaleVertical(geometry.marginBottom),
|
|
47
|
+
},
|
|
48
|
+
labelContainer: {
|
|
49
|
+
marginBottom: scaling.scaleVertical(geometry.labelGap),
|
|
50
|
+
},
|
|
51
|
+
field,
|
|
52
|
+
text,
|
|
53
|
+
multilineText: {
|
|
54
|
+
textAlignVertical: 'top',
|
|
55
|
+
paddingVertical: scaling.scaleVertical(geometry.multilinePaddingVertical),
|
|
56
|
+
},
|
|
57
|
+
startAccessory: {
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
justifyContent: 'center',
|
|
60
|
+
marginEnd: scaling.scaleHorizontal(geometry.startAccessoryGap),
|
|
61
|
+
},
|
|
62
|
+
endAccessory: {
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
justifyContent: 'center',
|
|
65
|
+
marginStart: scaling.scaleHorizontal(geometry.endAccessoryGap),
|
|
66
|
+
},
|
|
67
|
+
passwordToggle: {
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
marginStart: scaling.scaleHorizontal(geometry.passwordToggleGap),
|
|
71
|
+
},
|
|
72
|
+
supportingText: {
|
|
73
|
+
marginTop: scaling.scaleVertical(geometry.supportingGap),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return styles;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const getPasswordHitSlop = (scaling: ScalingFunctions) => scaling.scaleHorizontal(12);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ActivityIndicatorProps, StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface LoaderProps {
|
|
5
|
+
indicator?: ReactNode;
|
|
6
|
+
inline?: boolean;
|
|
7
|
+
transparent?: boolean;
|
|
8
|
+
color?: ActivityIndicatorProps['color'];
|
|
9
|
+
size?: ActivityIndicatorProps['size'];
|
|
10
|
+
accessibilityLabel?: string;
|
|
11
|
+
style?: StyleProp<ViewStyle>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
import { useUITheme } from '../../../theme';
|
|
4
|
+
import { getStyles } from './styles';
|
|
5
|
+
import type { LoaderProps } from '../types/types';
|
|
6
|
+
|
|
7
|
+
export const Loader = ({
|
|
8
|
+
indicator,
|
|
9
|
+
inline = false,
|
|
10
|
+
transparent = false,
|
|
11
|
+
color,
|
|
12
|
+
size = 'large',
|
|
13
|
+
accessibilityLabel,
|
|
14
|
+
style,
|
|
15
|
+
}: LoaderProps) => {
|
|
16
|
+
const { colors } = useUITheme();
|
|
17
|
+
const styles = useMemo(() => getStyles(colors, inline, transparent), [colors, inline, transparent]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
accessibilityRole="progressbar"
|
|
22
|
+
accessibilityLabel={accessibilityLabel}
|
|
23
|
+
accessibilityState={{ busy: true }}
|
|
24
|
+
style={[styles.container, style]}
|
|
25
|
+
>
|
|
26
|
+
{indicator === undefined ? <ActivityIndicator color={color ?? colors.primary} size={size} /> : indicator}
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
import type { UIColors } from '../../../theme';
|
|
3
|
+
|
|
4
|
+
export const getStyles = (colors: UIColors, inline: boolean, transparent: boolean) => StyleSheet.create({
|
|
5
|
+
container: {
|
|
6
|
+
zIndex: 5,
|
|
7
|
+
position: inline ? 'relative' : 'absolute',
|
|
8
|
+
width: '100%',
|
|
9
|
+
height: inline ? undefined : '100%',
|
|
10
|
+
backgroundColor: transparent ? colors.transparent : colors.background,
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { groupLoggerEntries, toggleExpandedEntryId } from '../utils/groupLoggerEntries';
|
|
3
|
+
import type { LoggerEntry } from '../types/types';
|
|
4
|
+
|
|
5
|
+
export const useLogger = (logs: readonly LoggerEntry[]) => {
|
|
6
|
+
const items = useMemo(() => groupLoggerEntries(logs), [logs]);
|
|
7
|
+
const [expandedEntryIds, setExpandedEntryIds] = useState<Set<string>>(() => new Set());
|
|
8
|
+
const onToggleEntry = useCallback((id: string) => {
|
|
9
|
+
setExpandedEntryIds(previous => toggleExpandedEntryId(previous, id));
|
|
10
|
+
}, []);
|
|
11
|
+
|
|
12
|
+
return { items, expandedEntryIds, onToggleEntry };
|
|
13
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export type LoggerEntryType = 'request' | 'response' | 'error' | 'library' | 'info' | 'warning';
|
|
5
|
+
|
|
6
|
+
export interface LoggerHttpMetadata {
|
|
7
|
+
/** Unique per HTTP exchange, including concurrent calls to the same endpoint. */
|
|
8
|
+
correlationId: string;
|
|
9
|
+
phase: 'request' | 'response';
|
|
10
|
+
status?: number;
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Sanitized, display-ready request data supplied by the application. */
|
|
15
|
+
export interface LoggerRequestData {
|
|
16
|
+
params?: string;
|
|
17
|
+
body?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoggerEntry {
|
|
21
|
+
id: string;
|
|
22
|
+
type: LoggerEntryType;
|
|
23
|
+
name: string;
|
|
24
|
+
message: string;
|
|
25
|
+
http?: LoggerHttpMetadata;
|
|
26
|
+
requestData?: LoggerRequestData;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LoggerLabels {
|
|
30
|
+
title: string;
|
|
31
|
+
clear: string;
|
|
32
|
+
close: string;
|
|
33
|
+
empty: string;
|
|
34
|
+
request: string;
|
|
35
|
+
response: string;
|
|
36
|
+
params: string;
|
|
37
|
+
body: string;
|
|
38
|
+
payload: string;
|
|
39
|
+
status: string;
|
|
40
|
+
copy: string;
|
|
41
|
+
typeLabels?: Partial<Record<LoggerEntryType, string>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LoggerProps {
|
|
45
|
+
/** Pass entries newest-first. Storage and retention remain application-owned. */
|
|
46
|
+
logs: readonly LoggerEntry[];
|
|
47
|
+
visible: boolean;
|
|
48
|
+
onClose: () => void;
|
|
49
|
+
onOpen?: () => void;
|
|
50
|
+
onClear?: () => void;
|
|
51
|
+
onCopyEntry?: (entry: LoggerEntry, text: string) => void | Promise<void>;
|
|
52
|
+
labels: LoggerLabels;
|
|
53
|
+
openIcon?: ReactNode;
|
|
54
|
+
copyIcon?: ReactNode;
|
|
55
|
+
style?: StyleProp<ViewStyle>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { FlatList, Modal, TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
|
4
|
+
import { useUITheme } from '../../../theme';
|
|
5
|
+
import { useScaling } from '../../../utils';
|
|
6
|
+
import { Typography } from '../../Typography';
|
|
7
|
+
import { LoggerItem } from './components/LoggerItem';
|
|
8
|
+
import { getStyles } from './styles';
|
|
9
|
+
import { useLogger } from '../presenters/useLogger';
|
|
10
|
+
import type { LoggerPresentationItem } from '../utils/groupLoggerEntries';
|
|
11
|
+
import type { LoggerProps } from '../types/types';
|
|
12
|
+
|
|
13
|
+
export const Logger = ({ logs, visible, onClose, onOpen, onClear, onCopyEntry, labels, openIcon, copyIcon, style }: LoggerProps) => {
|
|
14
|
+
const { colors, spacing, radius } = useUITheme();
|
|
15
|
+
const scaling = useScaling();
|
|
16
|
+
const styles = useMemo(() => getStyles(colors, spacing, radius, scaling), [colors, spacing, radius, scaling]);
|
|
17
|
+
const { items, expandedEntryIds, onToggleEntry } = useLogger(logs);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{onOpen ? (
|
|
22
|
+
<TouchableOpacity
|
|
23
|
+
accessibilityRole="button"
|
|
24
|
+
accessibilityLabel={labels.title}
|
|
25
|
+
onPress={onOpen}
|
|
26
|
+
style={styles.launcher}
|
|
27
|
+
>
|
|
28
|
+
{openIcon ?? <Typography text={labels.title} variant="label" />}
|
|
29
|
+
</TouchableOpacity>
|
|
30
|
+
) : null}
|
|
31
|
+
|
|
32
|
+
<Modal visible={visible} onRequestClose={onClose} animationType="slide">
|
|
33
|
+
<SafeAreaProvider>
|
|
34
|
+
<SafeAreaView style={[styles.container, style]}>
|
|
35
|
+
<View style={styles.header}>
|
|
36
|
+
<Typography text={labels.title} variant="title" style={styles.headerTitle} />
|
|
37
|
+
{onClear ? (
|
|
38
|
+
<TouchableOpacity
|
|
39
|
+
accessibilityRole="button"
|
|
40
|
+
accessibilityLabel={labels.clear}
|
|
41
|
+
onPress={onClear}
|
|
42
|
+
style={styles.headerAction}
|
|
43
|
+
>
|
|
44
|
+
<Typography text={labels.clear} variant="label" style={styles.actionText} />
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
) : null}
|
|
47
|
+
<TouchableOpacity
|
|
48
|
+
accessibilityRole="button"
|
|
49
|
+
accessibilityLabel={labels.close}
|
|
50
|
+
onPress={onClose}
|
|
51
|
+
style={styles.headerAction}
|
|
52
|
+
>
|
|
53
|
+
<Typography text={labels.close} variant="label" style={styles.actionText} />
|
|
54
|
+
</TouchableOpacity>
|
|
55
|
+
</View>
|
|
56
|
+
|
|
57
|
+
<FlatList<LoggerPresentationItem>
|
|
58
|
+
data={items}
|
|
59
|
+
keyExtractor={item => item.key}
|
|
60
|
+
renderItem={({ item }) => (
|
|
61
|
+
<LoggerItem
|
|
62
|
+
item={item}
|
|
63
|
+
labels={labels}
|
|
64
|
+
colors={colors}
|
|
65
|
+
styles={styles}
|
|
66
|
+
expandedEntryIds={expandedEntryIds}
|
|
67
|
+
onToggleEntry={onToggleEntry}
|
|
68
|
+
onCopyEntry={onCopyEntry}
|
|
69
|
+
copyIcon={copyIcon}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
style={styles.list}
|
|
73
|
+
contentContainerStyle={styles.listContent}
|
|
74
|
+
ListEmptyComponent={
|
|
75
|
+
<View style={styles.empty}>
|
|
76
|
+
<Typography text={labels.empty} variant="body" color="textMuted" />
|
|
77
|
+
</View>
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
</SafeAreaView>
|
|
81
|
+
</SafeAreaProvider>
|
|
82
|
+
</Modal>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text, TouchableOpacity, View } from 'react-native';
|
|
3
|
+
import type { UIColors } from '../../../../theme';
|
|
4
|
+
import { Typography } from '../../../Typography';
|
|
5
|
+
import type { LoggerPresentationItem } from '../../utils/groupLoggerEntries';
|
|
6
|
+
import {
|
|
7
|
+
getCopyableText,
|
|
8
|
+
getExpandablePayload,
|
|
9
|
+
getLoggerSectionContext,
|
|
10
|
+
getRequestParams,
|
|
11
|
+
getResponseDurationSuffix,
|
|
12
|
+
getResponseSummary,
|
|
13
|
+
getStatusColorKey,
|
|
14
|
+
getVisibleSectionText,
|
|
15
|
+
shouldRenderLoggerSection,
|
|
16
|
+
} from '../../utils/loggerPresentation';
|
|
17
|
+
import type { LoggerEntry, LoggerLabels, LoggerProps } from '../../types/types';
|
|
18
|
+
import type { getStyles } from '../styles';
|
|
19
|
+
|
|
20
|
+
interface LoggerItemProps {
|
|
21
|
+
item: LoggerPresentationItem;
|
|
22
|
+
labels: LoggerLabels;
|
|
23
|
+
colors: UIColors;
|
|
24
|
+
styles: ReturnType<typeof getStyles>;
|
|
25
|
+
expandedEntryIds: ReadonlySet<string>;
|
|
26
|
+
onToggleEntry: (id: string) => void;
|
|
27
|
+
onCopyEntry: LoggerProps['onCopyEntry'];
|
|
28
|
+
copyIcon: LoggerProps['copyIcon'];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sectionColor = (entry: LoggerEntry, colors: UIColors): string => {
|
|
32
|
+
if (entry.type === 'error' || (entry.http?.status !== undefined && entry.http.status >= 400)) return colors.error;
|
|
33
|
+
if (entry.type === 'warning') return colors.warning;
|
|
34
|
+
if (entry.http?.phase === 'response' || entry.type === 'response') return colors.success;
|
|
35
|
+
if (entry.http?.phase === 'request' || entry.type === 'request') return colors.primary;
|
|
36
|
+
return colors.icon;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const LoggerItem = ({ item, labels, colors, styles, expandedEntryIds, onToggleEntry, onCopyEntry, copyIcon }: LoggerItemProps) => {
|
|
40
|
+
const entries = item.kind === 'exchange'
|
|
41
|
+
? [item.request, item.response].filter((entry): entry is LoggerEntry => entry !== undefined && shouldRenderLoggerSection(entry, labels))
|
|
42
|
+
: shouldRenderLoggerSection(item.entry, labels) ? [item.entry] : [];
|
|
43
|
+
const name = item.kind === 'exchange' ? item.request?.name ?? item.response?.name : item.entry.name;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View style={styles.card}>
|
|
47
|
+
<Typography text={name ?? ''} variant="label" weight="bold" style={styles.cardTitle} />
|
|
48
|
+
{entries.map((entry, index) => {
|
|
49
|
+
const expanded = expandedEntryIds.has(entry.id);
|
|
50
|
+
const payload = getExpandablePayload(entry, labels);
|
|
51
|
+
const hasPayload = payload !== undefined;
|
|
52
|
+
const requestParams = getRequestParams(entry, labels);
|
|
53
|
+
const copyText = onCopyEntry ? getCopyableText(entry, labels) : undefined;
|
|
54
|
+
const context = getLoggerSectionContext(entry, labels);
|
|
55
|
+
const summary = getResponseSummary(entry, labels);
|
|
56
|
+
const visibleText = getVisibleSectionText(entry, labels);
|
|
57
|
+
const accessibilityLabel = `${context} ${entry.name}${summary ? ` ${summary}` : ''}`;
|
|
58
|
+
const status = entry.http?.phase === 'response' ? entry.http.status : undefined;
|
|
59
|
+
const visibleLabel = visibleText ? status === undefined ? (
|
|
60
|
+
<Typography text={visibleText} variant="caption" weight="medium" style={styles.sectionTitle} />
|
|
61
|
+
) : (
|
|
62
|
+
<Typography variant="caption" weight="medium" style={styles.sectionTitle}>
|
|
63
|
+
{`${labels.status}: `}
|
|
64
|
+
<Text style={{ color: colors[getStatusColorKey(status)] }}>{status}</Text>
|
|
65
|
+
{getResponseDurationSuffix(entry)}
|
|
66
|
+
</Typography>
|
|
67
|
+
) : <View style={styles.sectionTitle} />;
|
|
68
|
+
const copyButton = onCopyEntry && copyText !== undefined ? (
|
|
69
|
+
<TouchableOpacity
|
|
70
|
+
accessibilityRole="button"
|
|
71
|
+
accessibilityLabel={`${labels.copy} ${accessibilityLabel}`}
|
|
72
|
+
onPress={() => { onCopyEntry(entry, copyText); }}
|
|
73
|
+
style={requestParams ? styles.copyButtonTop : styles.copyButton}
|
|
74
|
+
>
|
|
75
|
+
{copyIcon ?? <Typography text={labels.copy} variant="caption" />}
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
) : null;
|
|
78
|
+
const showRow = hasPayload || !!visibleText || (!requestParams && copyButton !== null);
|
|
79
|
+
return (
|
|
80
|
+
<React.Fragment key={entry.id}>
|
|
81
|
+
{index > 0 ? <View style={styles.divider} /> : null}
|
|
82
|
+
<View style={[styles.section, { borderLeftColor: sectionColor(entry, colors) }]}>
|
|
83
|
+
{requestParams ? (
|
|
84
|
+
<View style={styles.paramsRow}>
|
|
85
|
+
<Typography text={requestParams} variant="caption" selectable style={[styles.message, styles.paramsText]} />
|
|
86
|
+
{copyButton}
|
|
87
|
+
</View>
|
|
88
|
+
) : null}
|
|
89
|
+
{showRow ? (
|
|
90
|
+
<View style={styles.sectionRow}>
|
|
91
|
+
{hasPayload ? (
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
accessibilityRole="button"
|
|
94
|
+
accessibilityLabel={accessibilityLabel}
|
|
95
|
+
accessibilityState={{ expanded }}
|
|
96
|
+
onPress={() => onToggleEntry(entry.id)}
|
|
97
|
+
style={styles.sectionButton}
|
|
98
|
+
>
|
|
99
|
+
{visibleLabel}
|
|
100
|
+
<Typography text={expanded ? '−' : '+'} variant="label" />
|
|
101
|
+
</TouchableOpacity>
|
|
102
|
+
) : visibleText ? (
|
|
103
|
+
<View accessible accessibilityLabel={accessibilityLabel} style={styles.sectionButton}>
|
|
104
|
+
{visibleLabel}
|
|
105
|
+
</View>
|
|
106
|
+
) : <View style={styles.sectionTitle} />}
|
|
107
|
+
{!requestParams ? copyButton : null}
|
|
108
|
+
</View>
|
|
109
|
+
) : requestParams ? null : <View accessible accessibilityLabel={accessibilityLabel} style={styles.emptySection} />}
|
|
110
|
+
{expanded && hasPayload ? <Typography text={payload} variant="body" selectable style={styles.message} /> : null}
|
|
111
|
+
</View>
|
|
112
|
+
</React.Fragment>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</View>
|
|
116
|
+
);
|
|
117
|
+
};
|