@zizwar/react-native-debug-console 1.0.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.ar.md +193 -0
- package/README.md +288 -0
- package/dist/DebugButton.d.ts +7 -0
- package/dist/DebugButton.js +58 -0
- package/dist/DebugConsole.d.ts +58 -0
- package/dist/DebugConsole.js +70 -0
- package/dist/DebugOverlay.d.ts +7 -0
- package/dist/DebugOverlay.js +176 -0
- package/dist/DebugProvider.d.ts +20 -0
- package/dist/DebugProvider.js +139 -0
- package/dist/Icons.d.ts +43 -0
- package/dist/Icons.js +37 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +33 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +29 -0
- package/package.json +52 -0
- package/src/DebugButton.tsx +84 -0
- package/src/DebugConsole.tsx +112 -0
- package/src/DebugOverlay.tsx +310 -0
- package/src/DebugProvider.tsx +182 -0
- package/src/Icons.tsx +91 -0
- package/src/index.ts +46 -0
- package/src/types.ts +102 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Debug Console - Floating Button
|
|
3
|
+
* A floating action button that shows error count and toggles the debug console
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import {
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
View,
|
|
10
|
+
Text,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Platform,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import { useDebugConsole } from './DebugProvider';
|
|
15
|
+
import { DebugIcon } from './Icons';
|
|
16
|
+
|
|
17
|
+
export const DebugButton: React.FC = () => {
|
|
18
|
+
const { toggleVisibility, logs, config } = useDebugConsole();
|
|
19
|
+
|
|
20
|
+
if (!config.enabled) return null;
|
|
21
|
+
|
|
22
|
+
const errorCount = logs.filter(l => l.type === 'error').length;
|
|
23
|
+
const position = config.buttonPosition;
|
|
24
|
+
const colors = config.colors;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<TouchableOpacity
|
|
28
|
+
style={[
|
|
29
|
+
styles.floatingButton,
|
|
30
|
+
{ backgroundColor: colors.buttonBackground },
|
|
31
|
+
position.bottom !== undefined && { bottom: position.bottom },
|
|
32
|
+
position.top !== undefined && { top: position.top },
|
|
33
|
+
position.right !== undefined && { right: position.right },
|
|
34
|
+
position.left !== undefined && { left: position.left },
|
|
35
|
+
]}
|
|
36
|
+
onPress={toggleVisibility}
|
|
37
|
+
activeOpacity={0.8}
|
|
38
|
+
>
|
|
39
|
+
<DebugIcon name="bug" size={20} color={colors.buttonIcon} />
|
|
40
|
+
{errorCount > 0 && (
|
|
41
|
+
<View style={styles.errorBadge}>
|
|
42
|
+
<Text style={[styles.errorBadgeText, { color: colors.buttonBackground }]}>
|
|
43
|
+
{errorCount > 9 ? '9+' : errorCount}
|
|
44
|
+
</Text>
|
|
45
|
+
</View>
|
|
46
|
+
)}
|
|
47
|
+
</TouchableOpacity>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const styles = StyleSheet.create({
|
|
52
|
+
floatingButton: {
|
|
53
|
+
position: 'absolute',
|
|
54
|
+
width: 50,
|
|
55
|
+
height: 50,
|
|
56
|
+
borderRadius: 25,
|
|
57
|
+
justifyContent: 'center',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
elevation: 5,
|
|
60
|
+
shadowColor: '#000',
|
|
61
|
+
shadowOffset: { width: 0, height: 2 },
|
|
62
|
+
shadowOpacity: 0.3,
|
|
63
|
+
shadowRadius: 4,
|
|
64
|
+
zIndex: 9999,
|
|
65
|
+
},
|
|
66
|
+
errorBadge: {
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: -4,
|
|
69
|
+
right: -4,
|
|
70
|
+
backgroundColor: '#fff',
|
|
71
|
+
borderRadius: 10,
|
|
72
|
+
minWidth: 20,
|
|
73
|
+
height: 20,
|
|
74
|
+
justifyContent: 'center',
|
|
75
|
+
alignItems: 'center',
|
|
76
|
+
paddingHorizontal: 4,
|
|
77
|
+
},
|
|
78
|
+
errorBadgeText: {
|
|
79
|
+
fontSize: 11,
|
|
80
|
+
fontWeight: 'bold',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export default DebugButton;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Debug Console - Main Component
|
|
3
|
+
* All-in-one wrapper that provides the debug console functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { ReactNode } from 'react';
|
|
7
|
+
import { View, StyleSheet } from 'react-native';
|
|
8
|
+
import { DebugProvider } from './DebugProvider';
|
|
9
|
+
import { DebugOverlay } from './DebugOverlay';
|
|
10
|
+
import { DebugButton } from './DebugButton';
|
|
11
|
+
import { DebugConsoleConfig } from './types';
|
|
12
|
+
|
|
13
|
+
interface DebugConsoleProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** Configuration options */
|
|
16
|
+
config?: DebugConsoleConfig;
|
|
17
|
+
/** Show the floating button (default: true) */
|
|
18
|
+
showButton?: boolean;
|
|
19
|
+
/** Custom floating button component */
|
|
20
|
+
customButton?: React.ComponentType;
|
|
21
|
+
/** Custom overlay component */
|
|
22
|
+
customOverlay?: React.ComponentType;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* DebugConsole - Main wrapper component
|
|
27
|
+
*
|
|
28
|
+
* Wrap your app with this component to enable debug console functionality.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* import { DebugConsole } from 'react-native-debug-console';
|
|
33
|
+
*
|
|
34
|
+
* export default function App() {
|
|
35
|
+
* return (
|
|
36
|
+
* <DebugConsole
|
|
37
|
+
* config={{
|
|
38
|
+
* enabled: process.env.EXPO_PUBLIC_DEBUG === 'true',
|
|
39
|
+
* appName: 'MyApp',
|
|
40
|
+
* }}
|
|
41
|
+
* >
|
|
42
|
+
* <YourApp />
|
|
43
|
+
* </DebugConsole>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const DebugConsole: React.FC<DebugConsoleProps> = ({
|
|
49
|
+
children,
|
|
50
|
+
config,
|
|
51
|
+
showButton = true,
|
|
52
|
+
customButton: CustomButton,
|
|
53
|
+
customOverlay: CustomOverlay,
|
|
54
|
+
}) => {
|
|
55
|
+
// If disabled, just render children without any debug UI
|
|
56
|
+
if (config?.enabled === false) {
|
|
57
|
+
return <>{children}</>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ButtonComponent = CustomButton || DebugButton;
|
|
61
|
+
const OverlayComponent = CustomOverlay || DebugOverlay;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<DebugProvider config={config}>
|
|
65
|
+
<View style={styles.container}>
|
|
66
|
+
{children}
|
|
67
|
+
{showButton && <ButtonComponent />}
|
|
68
|
+
<OverlayComponent />
|
|
69
|
+
</View>
|
|
70
|
+
</DebugProvider>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* withDebugConsole - HOC to wrap any component with debug console
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* import { withDebugConsole } from 'react-native-debug-console';
|
|
80
|
+
*
|
|
81
|
+
* const App = () => <YourApp />;
|
|
82
|
+
*
|
|
83
|
+
* export default withDebugConsole(App, {
|
|
84
|
+
* enabled: __DEV__,
|
|
85
|
+
* appName: 'MyApp',
|
|
86
|
+
* });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function withDebugConsole<P extends object>(
|
|
90
|
+
WrappedComponent: React.ComponentType<P>,
|
|
91
|
+
config?: DebugConsoleConfig
|
|
92
|
+
) {
|
|
93
|
+
const WithDebugConsole: React.FC<P> = (props) => {
|
|
94
|
+
return (
|
|
95
|
+
<DebugConsole config={config}>
|
|
96
|
+
<WrappedComponent {...props} />
|
|
97
|
+
</DebugConsole>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
WithDebugConsole.displayName = `WithDebugConsole(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
|
|
102
|
+
|
|
103
|
+
return WithDebugConsole;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
container: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export default DebugConsole;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Debug Console - Overlay
|
|
3
|
+
* Full-screen modal displaying captured logs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import {
|
|
8
|
+
View,
|
|
9
|
+
Text,
|
|
10
|
+
ScrollView,
|
|
11
|
+
TouchableOpacity,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Modal,
|
|
14
|
+
SafeAreaView,
|
|
15
|
+
Share,
|
|
16
|
+
Platform,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { useDebugConsole } from './DebugProvider';
|
|
19
|
+
import { LogEntry } from './types';
|
|
20
|
+
import { DebugIcon } from './Icons';
|
|
21
|
+
|
|
22
|
+
interface LogEntryItemProps {
|
|
23
|
+
entry: LogEntry;
|
|
24
|
+
colors: {
|
|
25
|
+
surface: string;
|
|
26
|
+
text: string;
|
|
27
|
+
textSecondary: string;
|
|
28
|
+
error: string;
|
|
29
|
+
warning: string;
|
|
30
|
+
info: string;
|
|
31
|
+
log: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LogEntryItem: React.FC<LogEntryItemProps> = ({ entry, colors }) => {
|
|
36
|
+
const getTypeColor = (type: LogEntry['type']) => {
|
|
37
|
+
switch (type) {
|
|
38
|
+
case 'error': return colors.error;
|
|
39
|
+
case 'warn': return colors.warning;
|
|
40
|
+
case 'info': return colors.info;
|
|
41
|
+
default: return colors.textSecondary;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getTypeIcon = (type: LogEntry['type']): 'close-circle' | 'warning' | 'information-circle' | 'ellipse' => {
|
|
46
|
+
switch (type) {
|
|
47
|
+
case 'error': return 'close-circle';
|
|
48
|
+
case 'warn': return 'warning';
|
|
49
|
+
case 'info': return 'information-circle';
|
|
50
|
+
default: return 'ellipse';
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatTime = (date: Date) => {
|
|
55
|
+
return date.toLocaleTimeString('en-US', {
|
|
56
|
+
hour12: false,
|
|
57
|
+
hour: '2-digit',
|
|
58
|
+
minute: '2-digit',
|
|
59
|
+
second: '2-digit',
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View style={[
|
|
65
|
+
styles.logEntry,
|
|
66
|
+
{ backgroundColor: colors.surface, borderLeftColor: getTypeColor(entry.type) }
|
|
67
|
+
]}>
|
|
68
|
+
<View style={styles.logHeader}>
|
|
69
|
+
<DebugIcon name={getTypeIcon(entry.type)} size={14} color={getTypeColor(entry.type)} />
|
|
70
|
+
<Text style={[styles.logTime, { color: colors.textSecondary }]}>
|
|
71
|
+
{formatTime(entry.timestamp)}
|
|
72
|
+
</Text>
|
|
73
|
+
<Text style={[styles.logType, { color: getTypeColor(entry.type) }]}>
|
|
74
|
+
{entry.type.toUpperCase()}
|
|
75
|
+
</Text>
|
|
76
|
+
</View>
|
|
77
|
+
<Text style={[styles.logMessage, { color: colors.text }]} selectable>
|
|
78
|
+
{entry.message}
|
|
79
|
+
</Text>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const DebugOverlay: React.FC = () => {
|
|
85
|
+
const { logs, isVisible, hideConsole, clearLogs, config } = useDebugConsole();
|
|
86
|
+
const colors = config.colors;
|
|
87
|
+
|
|
88
|
+
const handleShare = async () => {
|
|
89
|
+
const logText = logs.map(log =>
|
|
90
|
+
`[${log.timestamp.toISOString()}] [${log.type.toUpperCase()}] ${log.message}`
|
|
91
|
+
).join('\n');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await Share.share({
|
|
95
|
+
message: logText,
|
|
96
|
+
title: `${config.appName} Debug Logs`,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// Silently fail if sharing not available
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!isVisible || !config.enabled) return null;
|
|
104
|
+
|
|
105
|
+
const errorCount = logs.filter(l => l.type === 'error').length;
|
|
106
|
+
const warnCount = logs.filter(l => l.type === 'warn').length;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Modal
|
|
110
|
+
visible={isVisible}
|
|
111
|
+
animationType="slide"
|
|
112
|
+
transparent={false}
|
|
113
|
+
onRequestClose={hideConsole}
|
|
114
|
+
>
|
|
115
|
+
<SafeAreaView style={[styles.container, { backgroundColor: colors.background }]}>
|
|
116
|
+
{/* Header */}
|
|
117
|
+
<View style={[styles.header, { borderBottomColor: colors.surface }]}>
|
|
118
|
+
<View style={styles.headerLeft}>
|
|
119
|
+
<DebugIcon name="bug" size={24} color={colors.error} />
|
|
120
|
+
<Text style={[styles.headerTitle, { color: colors.text }]}>Debug Console</Text>
|
|
121
|
+
</View>
|
|
122
|
+
<View style={styles.headerStats}>
|
|
123
|
+
{errorCount > 0 && (
|
|
124
|
+
<View style={[styles.statBadge, { backgroundColor: colors.error }]}>
|
|
125
|
+
<Text style={styles.statText}>{errorCount} errors</Text>
|
|
126
|
+
</View>
|
|
127
|
+
)}
|
|
128
|
+
{warnCount > 0 && (
|
|
129
|
+
<View style={[styles.statBadge, { backgroundColor: colors.warning }]}>
|
|
130
|
+
<Text style={[styles.statText, { color: '#000' }]}>{warnCount} warns</Text>
|
|
131
|
+
</View>
|
|
132
|
+
)}
|
|
133
|
+
</View>
|
|
134
|
+
<TouchableOpacity onPress={hideConsole} style={styles.closeButton}>
|
|
135
|
+
<DebugIcon name="close" size={24} color={colors.text} />
|
|
136
|
+
</TouchableOpacity>
|
|
137
|
+
</View>
|
|
138
|
+
|
|
139
|
+
{/* Actions */}
|
|
140
|
+
<View style={[styles.actions, { borderBottomColor: colors.surface }]}>
|
|
141
|
+
<TouchableOpacity
|
|
142
|
+
style={[styles.actionButton, { backgroundColor: colors.surface }]}
|
|
143
|
+
onPress={clearLogs}
|
|
144
|
+
>
|
|
145
|
+
<DebugIcon name="trash" size={18} color={colors.error} />
|
|
146
|
+
<Text style={[styles.actionText, { color: colors.text }]}>Clear</Text>
|
|
147
|
+
</TouchableOpacity>
|
|
148
|
+
<TouchableOpacity
|
|
149
|
+
style={[styles.actionButton, { backgroundColor: colors.surface }]}
|
|
150
|
+
onPress={handleShare}
|
|
151
|
+
>
|
|
152
|
+
<DebugIcon name="share" size={18} color={colors.info} />
|
|
153
|
+
<Text style={[styles.actionText, { color: colors.text }]}>Share</Text>
|
|
154
|
+
</TouchableOpacity>
|
|
155
|
+
<Text style={[styles.logCount, { color: colors.textSecondary }]}>
|
|
156
|
+
{logs.length} logs
|
|
157
|
+
</Text>
|
|
158
|
+
</View>
|
|
159
|
+
|
|
160
|
+
{/* Logs */}
|
|
161
|
+
<ScrollView
|
|
162
|
+
style={styles.logsContainer}
|
|
163
|
+
contentContainerStyle={styles.logsContent}
|
|
164
|
+
showsVerticalScrollIndicator={true}
|
|
165
|
+
>
|
|
166
|
+
{logs.length === 0 ? (
|
|
167
|
+
<View style={styles.emptyState}>
|
|
168
|
+
<DebugIcon name="document" size={48} color={colors.textSecondary} />
|
|
169
|
+
<Text style={[styles.emptyText, { color: colors.textSecondary }]}>
|
|
170
|
+
No logs yet
|
|
171
|
+
</Text>
|
|
172
|
+
<Text style={[styles.emptySubtext, { color: colors.textSecondary }]}>
|
|
173
|
+
Console logs will appear here
|
|
174
|
+
</Text>
|
|
175
|
+
</View>
|
|
176
|
+
) : (
|
|
177
|
+
logs.map(entry => (
|
|
178
|
+
<LogEntryItem key={entry.id} entry={entry} colors={colors} />
|
|
179
|
+
))
|
|
180
|
+
)}
|
|
181
|
+
</ScrollView>
|
|
182
|
+
|
|
183
|
+
{/* Footer */}
|
|
184
|
+
<View style={[styles.footer, { borderTopColor: colors.surface }]}>
|
|
185
|
+
<Text style={[styles.footerText, { color: colors.textSecondary }]}>
|
|
186
|
+
Tap on log to copy • {config.appName}
|
|
187
|
+
</Text>
|
|
188
|
+
</View>
|
|
189
|
+
</SafeAreaView>
|
|
190
|
+
</Modal>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const styles = StyleSheet.create({
|
|
195
|
+
container: {
|
|
196
|
+
flex: 1,
|
|
197
|
+
},
|
|
198
|
+
header: {
|
|
199
|
+
flexDirection: 'row',
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
justifyContent: 'space-between',
|
|
202
|
+
paddingHorizontal: 16,
|
|
203
|
+
paddingVertical: 12,
|
|
204
|
+
borderBottomWidth: 1,
|
|
205
|
+
},
|
|
206
|
+
headerLeft: {
|
|
207
|
+
flexDirection: 'row',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
gap: 8,
|
|
210
|
+
},
|
|
211
|
+
headerTitle: {
|
|
212
|
+
fontSize: 18,
|
|
213
|
+
fontWeight: 'bold',
|
|
214
|
+
},
|
|
215
|
+
headerStats: {
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
gap: 8,
|
|
218
|
+
},
|
|
219
|
+
statBadge: {
|
|
220
|
+
paddingHorizontal: 8,
|
|
221
|
+
paddingVertical: 4,
|
|
222
|
+
borderRadius: 12,
|
|
223
|
+
},
|
|
224
|
+
statText: {
|
|
225
|
+
fontSize: 12,
|
|
226
|
+
fontWeight: 'bold',
|
|
227
|
+
color: '#fff',
|
|
228
|
+
},
|
|
229
|
+
closeButton: {
|
|
230
|
+
padding: 8,
|
|
231
|
+
},
|
|
232
|
+
actions: {
|
|
233
|
+
flexDirection: 'row',
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
paddingHorizontal: 16,
|
|
236
|
+
paddingVertical: 8,
|
|
237
|
+
borderBottomWidth: 1,
|
|
238
|
+
gap: 16,
|
|
239
|
+
},
|
|
240
|
+
actionButton: {
|
|
241
|
+
flexDirection: 'row',
|
|
242
|
+
alignItems: 'center',
|
|
243
|
+
gap: 6,
|
|
244
|
+
paddingVertical: 6,
|
|
245
|
+
paddingHorizontal: 12,
|
|
246
|
+
borderRadius: 8,
|
|
247
|
+
},
|
|
248
|
+
actionText: {
|
|
249
|
+
fontSize: 14,
|
|
250
|
+
},
|
|
251
|
+
logCount: {
|
|
252
|
+
marginLeft: 'auto',
|
|
253
|
+
fontSize: 12,
|
|
254
|
+
},
|
|
255
|
+
logsContainer: {
|
|
256
|
+
flex: 1,
|
|
257
|
+
},
|
|
258
|
+
logsContent: {
|
|
259
|
+
padding: 8,
|
|
260
|
+
},
|
|
261
|
+
logEntry: {
|
|
262
|
+
borderRadius: 8,
|
|
263
|
+
padding: 10,
|
|
264
|
+
marginBottom: 8,
|
|
265
|
+
borderLeftWidth: 3,
|
|
266
|
+
},
|
|
267
|
+
logHeader: {
|
|
268
|
+
flexDirection: 'row',
|
|
269
|
+
alignItems: 'center',
|
|
270
|
+
gap: 8,
|
|
271
|
+
marginBottom: 4,
|
|
272
|
+
},
|
|
273
|
+
logTime: {
|
|
274
|
+
fontSize: 11,
|
|
275
|
+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
276
|
+
},
|
|
277
|
+
logType: {
|
|
278
|
+
fontSize: 10,
|
|
279
|
+
fontWeight: 'bold',
|
|
280
|
+
},
|
|
281
|
+
logMessage: {
|
|
282
|
+
fontSize: 12,
|
|
283
|
+
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
|
|
284
|
+
lineHeight: 18,
|
|
285
|
+
},
|
|
286
|
+
emptyState: {
|
|
287
|
+
alignItems: 'center',
|
|
288
|
+
justifyContent: 'center',
|
|
289
|
+
paddingVertical: 60,
|
|
290
|
+
},
|
|
291
|
+
emptyText: {
|
|
292
|
+
marginTop: 12,
|
|
293
|
+
fontSize: 16,
|
|
294
|
+
fontWeight: '600',
|
|
295
|
+
},
|
|
296
|
+
emptySubtext: {
|
|
297
|
+
marginTop: 4,
|
|
298
|
+
fontSize: 14,
|
|
299
|
+
},
|
|
300
|
+
footer: {
|
|
301
|
+
padding: 12,
|
|
302
|
+
borderTopWidth: 1,
|
|
303
|
+
alignItems: 'center',
|
|
304
|
+
},
|
|
305
|
+
footerText: {
|
|
306
|
+
fontSize: 12,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
export default DebugOverlay;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Debug Console - Provider
|
|
3
|
+
* Context provider that captures console logs and manages state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, {
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
useState,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useMemo,
|
|
13
|
+
ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import {
|
|
16
|
+
LogEntry,
|
|
17
|
+
LogType,
|
|
18
|
+
DebugConsoleConfig,
|
|
19
|
+
DebugConsoleContextType,
|
|
20
|
+
DEFAULT_CONFIG,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
const DebugConsoleContext = createContext<DebugConsoleContextType | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
// Store original console methods
|
|
26
|
+
const originalConsole = {
|
|
27
|
+
log: console.log.bind(console),
|
|
28
|
+
warn: console.warn.bind(console),
|
|
29
|
+
error: console.error.bind(console),
|
|
30
|
+
info: console.info.bind(console),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface DebugProviderProps {
|
|
34
|
+
children: ReactNode;
|
|
35
|
+
config?: DebugConsoleConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DebugProvider: React.FC<DebugProviderProps> = ({ children, config: userConfig }) => {
|
|
39
|
+
// Merge user config with defaults
|
|
40
|
+
const config = useMemo<Required<DebugConsoleConfig>>(() => {
|
|
41
|
+
return {
|
|
42
|
+
...DEFAULT_CONFIG,
|
|
43
|
+
...userConfig,
|
|
44
|
+
colors: {
|
|
45
|
+
...DEFAULT_CONFIG.colors,
|
|
46
|
+
...userConfig?.colors,
|
|
47
|
+
},
|
|
48
|
+
buttonPosition: {
|
|
49
|
+
...DEFAULT_CONFIG.buttonPosition,
|
|
50
|
+
...userConfig?.buttonPosition,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}, [userConfig]);
|
|
54
|
+
|
|
55
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
56
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
57
|
+
|
|
58
|
+
const addLog = useCallback((type: LogType, message: string, data?: any) => {
|
|
59
|
+
const entry: LogEntry = {
|
|
60
|
+
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
61
|
+
timestamp: new Date(),
|
|
62
|
+
type,
|
|
63
|
+
message: typeof message === 'string' ? message : JSON.stringify(message),
|
|
64
|
+
data,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
setLogs(prev => {
|
|
68
|
+
const newLogs = [...prev, entry];
|
|
69
|
+
// Keep only last N logs based on config
|
|
70
|
+
if (newLogs.length > config.maxLogs) {
|
|
71
|
+
return newLogs.slice(-config.maxLogs);
|
|
72
|
+
}
|
|
73
|
+
return newLogs;
|
|
74
|
+
});
|
|
75
|
+
}, [config.maxLogs]);
|
|
76
|
+
|
|
77
|
+
const clearLogs = useCallback(() => {
|
|
78
|
+
setLogs([]);
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const toggleVisibility = useCallback(() => {
|
|
82
|
+
setIsVisible(prev => !prev);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const showConsole = useCallback(() => {
|
|
86
|
+
setIsVisible(true);
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const hideConsole = useCallback(() => {
|
|
90
|
+
setIsVisible(false);
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Override console methods to capture logs
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!config.enabled) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const formatArgs = (args: any[]): string => {
|
|
100
|
+
return args.map(arg => {
|
|
101
|
+
if (typeof arg === 'object') {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.stringify(arg, null, 2);
|
|
104
|
+
} catch {
|
|
105
|
+
return String(arg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return String(arg);
|
|
109
|
+
}).join(' ');
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const wrapConsole = (type: LogType, originalFn: (...args: any[]) => void) => {
|
|
113
|
+
return (...args: any[]) => {
|
|
114
|
+
// Always call original console method
|
|
115
|
+
originalFn(...args);
|
|
116
|
+
|
|
117
|
+
// Add to our logs
|
|
118
|
+
const message = formatArgs(args);
|
|
119
|
+
addLog(type, message);
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Override console methods based on config
|
|
124
|
+
if (config.captureLog) {
|
|
125
|
+
console.log = wrapConsole('log', originalConsole.log);
|
|
126
|
+
}
|
|
127
|
+
if (config.captureWarn) {
|
|
128
|
+
console.warn = wrapConsole('warn', originalConsole.warn);
|
|
129
|
+
}
|
|
130
|
+
if (config.captureError) {
|
|
131
|
+
console.error = wrapConsole('error', originalConsole.error);
|
|
132
|
+
}
|
|
133
|
+
if (config.captureInfo) {
|
|
134
|
+
console.info = wrapConsole('info', originalConsole.info);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Restore original methods on cleanup
|
|
138
|
+
return () => {
|
|
139
|
+
console.log = originalConsole.log;
|
|
140
|
+
console.warn = originalConsole.warn;
|
|
141
|
+
console.error = originalConsole.error;
|
|
142
|
+
console.info = originalConsole.info;
|
|
143
|
+
};
|
|
144
|
+
}, [config.enabled, config.captureLog, config.captureWarn, config.captureError, config.captureInfo, addLog]);
|
|
145
|
+
|
|
146
|
+
const contextValue = useMemo<DebugConsoleContextType>(() => ({
|
|
147
|
+
logs,
|
|
148
|
+
isVisible,
|
|
149
|
+
config,
|
|
150
|
+
addLog,
|
|
151
|
+
clearLogs,
|
|
152
|
+
toggleVisibility,
|
|
153
|
+
showConsole,
|
|
154
|
+
hideConsole,
|
|
155
|
+
}), [logs, isVisible, config, addLog, clearLogs, toggleVisibility, showConsole, hideConsole]);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<DebugConsoleContext.Provider value={contextValue}>
|
|
159
|
+
{children}
|
|
160
|
+
</DebugConsoleContext.Provider>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Hook to access the debug console context
|
|
166
|
+
*/
|
|
167
|
+
export const useDebugConsole = (): DebugConsoleContextType => {
|
|
168
|
+
const context = useContext(DebugConsoleContext);
|
|
169
|
+
if (!context) {
|
|
170
|
+
throw new Error('useDebugConsole must be used within a DebugProvider');
|
|
171
|
+
}
|
|
172
|
+
return context;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Optional hook that doesn't throw if used outside provider
|
|
177
|
+
*/
|
|
178
|
+
export const useDebugConsoleOptional = (): DebugConsoleContextType | null => {
|
|
179
|
+
return useContext(DebugConsoleContext) ?? null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default DebugProvider;
|