react-native-debug-toolkit 0.2.2 → 0.3.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.
@@ -0,0 +1,279 @@
1
+ import React, { useState, useMemo } from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ FlatList,
7
+ TouchableOpacity,
8
+ } from 'react-native'
9
+ import ZustandLogDetails from './ZustandLogDetails'
10
+ import { getZustandActionColor } from '../utils/DebugConst'
11
+
12
+ const SubViewZustandLogs = ({ logs = [] }) => {
13
+ const [selectedLog, setSelectedLog] = useState(null)
14
+
15
+ // Memoize the sorted logs
16
+ const sortedLogs = useMemo(() => {
17
+ // Create a stable copy before sorting
18
+ return [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
19
+ }, [logs]);
20
+
21
+ // Helper to format preview of state changes
22
+ const formatActionPreview = (log) => {
23
+ if (!log) return '';
24
+
25
+ const { action = 'unknown', storeName } = log;
26
+ return `Action: ${action}${storeName ? ` (${storeName})` : ''}`;
27
+ };
28
+
29
+ // Safely stringify value for display
30
+ const safeStringify = (value) => {
31
+ if (value === undefined) return 'undefined';
32
+ if (value === null) return 'null';
33
+
34
+ const type = typeof value;
35
+ if (type === 'string') return `"${value.length > 10 ? value.substring(0, 10) + '...' : value}"`;
36
+ if (type === 'number' || type === 'boolean') return String(value);
37
+ if (Array.isArray(value)) return '[...]';
38
+ if (type === 'object') return '{...}';
39
+
40
+ return String(value);
41
+ };
42
+
43
+ // Helper to find key changes between states with more details
44
+ const findChangedKeys = (prevState, nextState) => {
45
+ if (!prevState || !nextState) return [];
46
+
47
+ const changes = [];
48
+ const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
49
+
50
+ allKeys.forEach(key => {
51
+ const prevValue = prevState[key];
52
+ const nextValue = nextState[key];
53
+
54
+ // Check if key is added, removed, or value changed
55
+ if (!(key in prevState)) {
56
+ changes.push({
57
+ key,
58
+ type: 'added',
59
+ display: `+${key}: ${safeStringify(nextValue)}`
60
+ });
61
+ } else if (!(key in nextState)) {
62
+ changes.push({
63
+ key,
64
+ type: 'removed',
65
+ display: `-${key}: ${safeStringify(prevValue)}`
66
+ });
67
+ } else if (JSON.stringify(prevValue) !== JSON.stringify(nextValue)) {
68
+ // For primitive types, show the before and after values
69
+ const isPrimitive =
70
+ typeof prevValue !== 'object' ||
71
+ prevValue === null ||
72
+ typeof nextValue !== 'object' ||
73
+ nextValue === null;
74
+
75
+ if (isPrimitive) {
76
+ changes.push({
77
+ key,
78
+ type: 'changed',
79
+ display: `~${key}: ${safeStringify(prevValue)} → ${safeStringify(nextValue)}`
80
+ });
81
+ } else {
82
+ changes.push({
83
+ key,
84
+ type: 'changed',
85
+ display: `~${key}`
86
+ });
87
+ }
88
+ }
89
+ });
90
+
91
+ return changes;
92
+ };
93
+
94
+ const renderLogItem = ({ item }) => {
95
+ const actionPreview = formatActionPreview(item);
96
+ const actionColor = getZustandActionColor(item.action);
97
+ const timestamp = item.timestamp
98
+ ? new Date(item.timestamp).toLocaleTimeString([], {
99
+ hour: '2-digit',
100
+ minute: '2-digit',
101
+ second: '2-digit',
102
+ hour12: false
103
+ })
104
+ : '';
105
+
106
+ // Get changed keys with details
107
+ const changes = findChangedKeys(item.prevState, item.nextState);
108
+
109
+ // Create a formatted display of changes
110
+ const changesText = changes.length
111
+ ? changes.map(change => change.display).join(', ')
112
+ : 'No changes detected';
113
+
114
+ return (
115
+ <TouchableOpacity
116
+ style={styles.logItem}
117
+ onPress={() => setSelectedLog(item)}>
118
+ <View style={styles.logItemContainer}>
119
+ <View
120
+ style={[styles.actionIndicator, { backgroundColor: actionColor }]}
121
+ />
122
+
123
+ <View style={styles.logContent}>
124
+ <View style={styles.logHeader}>
125
+ <Text style={[styles.actionText, { color: actionColor }]}>
126
+ {actionPreview}
127
+ </Text>
128
+ <Text style={styles.time}>{timestamp}</Text>
129
+ </View>
130
+
131
+ <Text style={styles.statePathsText} numberOfLines={2}>
132
+ {changesText}
133
+ </Text>
134
+ </View>
135
+ </View>
136
+ </TouchableOpacity>
137
+ )
138
+ }
139
+
140
+ // If a log is selected, show the details view
141
+ if (selectedLog) {
142
+ const timestamp = selectedLog.timestamp
143
+ ? new Date(selectedLog.timestamp).toLocaleTimeString([], {
144
+ hour: '2-digit',
145
+ minute: '2-digit',
146
+ second: '2-digit',
147
+ hour12: false
148
+ })
149
+ : '';
150
+ const actionColor = getZustandActionColor(selectedLog.action);
151
+ const actionDisplay = selectedLog.storeName
152
+ ? `${selectedLog.action} (${selectedLog.storeName})`
153
+ : selectedLog.action || 'Unknown Action';
154
+
155
+ return (
156
+ <View style={styles.container}>
157
+ <View style={styles.detailsHeader}>
158
+ <TouchableOpacity
159
+ style={styles.backButton}
160
+ onPress={() => setSelectedLog(null)}>
161
+ <Text style={styles.backButtonText}>← Back</Text>
162
+ </TouchableOpacity>
163
+ <View style={styles.headerActionTime}>
164
+ <Text style={[styles.headerAction, { color: actionColor }]}>
165
+ {actionDisplay}
166
+ </Text>
167
+ <Text style={styles.headerTimestamp}>{timestamp}</Text>
168
+ </View>
169
+ </View>
170
+ <ZustandLogDetails log={selectedLog} />
171
+ </View>
172
+ )
173
+ }
174
+
175
+ // Otherwise show the list view
176
+ return (
177
+ <View style={styles.container}>
178
+ {sortedLogs.length === 0 ? (
179
+ <Text style={styles.emptyText}>No Zustand state changes logged yet</Text>
180
+ ) : (
181
+ <FlatList
182
+ data={sortedLogs}
183
+ renderItem={renderLogItem}
184
+ keyExtractor={(item, index) => `${item.timestamp}-${index}`}
185
+ style={styles.list}
186
+ />
187
+ )}
188
+ </View>
189
+ )
190
+ }
191
+
192
+ // Adapted styles from SubViewConsoleLogs
193
+ const styles = StyleSheet.create({
194
+ container: {
195
+ flex: 1,
196
+ backgroundColor: '#fff',
197
+ },
198
+ list: {
199
+ flex: 1,
200
+ },
201
+ emptyText: {
202
+ textAlign: 'center',
203
+ color: '#999',
204
+ marginTop: 20,
205
+ },
206
+ logItem: {
207
+ borderBottomWidth: 1,
208
+ borderBottomColor: '#eee',
209
+ },
210
+ logItemContainer: {
211
+ flexDirection: 'row',
212
+ paddingVertical: 10,
213
+ paddingHorizontal: 12,
214
+ },
215
+ actionIndicator: {
216
+ width: 4,
217
+ borderRadius: 2,
218
+ marginRight: 10,
219
+ },
220
+ logContent: {
221
+ flex: 1,
222
+ },
223
+ logHeader: {
224
+ flexDirection: 'row',
225
+ justifyContent: 'space-between',
226
+ alignItems: 'center',
227
+ marginBottom: 5,
228
+ },
229
+ actionText: {
230
+ fontSize: 13,
231
+ fontWeight: 'bold',
232
+ },
233
+ time: {
234
+ fontSize: 12,
235
+ color: '#888',
236
+ },
237
+ statePathsText: {
238
+ fontSize: 14,
239
+ color: '#555',
240
+ fontFamily: 'monospace',
241
+ },
242
+ // Details Header Styles
243
+ detailsHeader: {
244
+ flexDirection: 'row',
245
+ alignItems: 'center',
246
+ paddingVertical: 10,
247
+ paddingHorizontal: 15,
248
+ borderBottomWidth: 1,
249
+ borderBottomColor: '#eee',
250
+ backgroundColor: '#f8f9fa',
251
+ },
252
+ backButton: {
253
+ paddingHorizontal: 10,
254
+ paddingVertical: 5,
255
+ borderRadius: 4,
256
+ backgroundColor: '#eee',
257
+ marginRight: 15,
258
+ },
259
+ backButtonText: {
260
+ color: '#333',
261
+ fontWeight: '500',
262
+ fontSize: 14,
263
+ },
264
+ headerActionTime: {
265
+ flexDirection: 'row',
266
+ alignItems: 'baseline',
267
+ },
268
+ headerAction: {
269
+ fontSize: 15,
270
+ fontWeight: 'bold',
271
+ marginRight: 8,
272
+ },
273
+ headerTimestamp: {
274
+ fontSize: 13,
275
+ color: '#666',
276
+ },
277
+ })
278
+
279
+ export default SubViewZustandLogs
@@ -0,0 +1,355 @@
1
+ import React from 'react'
2
+ import { View, Text, StyleSheet, Clipboard } from 'react-native'
3
+ import { ScrollView, Pressable } from 'react-native'
4
+ import JSONTree from 'react-native-json-tree'
5
+ import { getZustandActionColor } from '../utils/DebugConst'
6
+
7
+ // Re-using the theme from ConsoleLogDetails for consistency
8
+ const theme = {
9
+ scheme: 'monokai',
10
+ author: 'wimer hazenberg (http://www.monokai.nl)',
11
+ base00: '#272822',
12
+ base01: '#383830',
13
+ base02: '#49483e',
14
+ base03: '#75715e',
15
+ base04: '#a59f85',
16
+ base05: '#f8f8f2',
17
+ base06: '#f5f4f1',
18
+ base07: '#f9f8f5',
19
+ base08: '#f92672',
20
+ base09: '#fd971f',
21
+ base0A: '#f4bf75',
22
+ base0B: '#a6e22e',
23
+ base0C: '#a1efe4',
24
+ base0D: '#66d9ef',
25
+ base0E: '#ae81ff',
26
+ base0F: '#cc6633'
27
+ };
28
+
29
+ const CopyButton = ({ text, style }) => {
30
+ const [copied, setCopied] = React.useState(false)
31
+
32
+ const handleCopy = async () => {
33
+ await Clipboard.setString(text)
34
+ setCopied(true)
35
+ setTimeout(() => setCopied(false), 2000)
36
+ }
37
+
38
+ return (
39
+ <Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
40
+ <Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
41
+ </Pressable>
42
+ )
43
+ }
44
+
45
+ const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
46
+ const [expanded, setExpanded] = React.useState(initiallyExpanded)
47
+
48
+ return (
49
+ <View style={styles.collapsibleSection}>
50
+ <Pressable
51
+ style={styles.sectionHeader}
52
+ onPress={() => setExpanded(!expanded)}>
53
+ <Text style={styles.sectionTitle}>{title}</Text>
54
+ <Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
55
+ </Pressable>
56
+ {expanded && children}
57
+ </View>
58
+ )
59
+ }
60
+
61
+ const ZustandLogDetails = ({ log }) => {
62
+ if (!log) {
63
+ return (
64
+ <View style={styles.errorContainer}>
65
+ <Text style={styles.errorText}>Log data is missing</Text>
66
+ </View>
67
+ )
68
+ }
69
+
70
+ const { timestamp, action, prevState, nextState, actionCompleteTime, storeName } = log
71
+ const actionColor = getZustandActionColor(action);
72
+
73
+ // Format data for display and copying
74
+ const formatStateToString = (state) => {
75
+ try {
76
+ return JSON.stringify(state, null, 2);
77
+ } catch (e) {
78
+ return '[unserializable state]';
79
+ }
80
+ }
81
+
82
+ const formattedPrevState = formatStateToString(prevState);
83
+ const formattedNextState = formatStateToString(nextState);
84
+
85
+ // Format for the full log copy
86
+ const formattedLog = `Action: ${action}${storeName ? ` (${storeName})` : ''}
87
+ Previous State: ${formattedPrevState}
88
+ Next State: ${formattedNextState}${actionCompleteTime ? `\nAction Complete Time: ${actionCompleteTime}ms` : ''}`;
89
+
90
+ // Function to find differences between states
91
+ const findDifferences = () => {
92
+ const changes = [];
93
+
94
+ if (!prevState || !nextState) return changes;
95
+
96
+ // Helper to find differences in objects recursively
97
+ const findChangesRecursive = (prev, next, path = '') => {
98
+ if (prev === next) return;
99
+
100
+ if (typeof prev !== 'object' || typeof next !== 'object' ||
101
+ prev === null || next === null) {
102
+ changes.push({
103
+ path: path || 'root',
104
+ prevValue: prev,
105
+ nextValue: next
106
+ });
107
+ return;
108
+ }
109
+
110
+ // Check for object keys
111
+ const allKeys = [...new Set([...Object.keys(prev || {}), ...Object.keys(next || {})])];
112
+
113
+ for (const key of allKeys) {
114
+ const newPath = path ? `${path}.${key}` : key;
115
+ if (!(key in prev)) {
116
+ changes.push({
117
+ path: newPath,
118
+ prevValue: undefined,
119
+ nextValue: next[key]
120
+ });
121
+ } else if (!(key in next)) {
122
+ changes.push({
123
+ path: newPath,
124
+ prevValue: prev[key],
125
+ nextValue: undefined
126
+ });
127
+ } else {
128
+ findChangesRecursive(prev[key], next[key], newPath);
129
+ }
130
+ }
131
+ };
132
+
133
+ findChangesRecursive(prevState, nextState);
134
+ return changes;
135
+ };
136
+
137
+ const stateChanges = findDifferences();
138
+ const hasChanges = stateChanges.length > 0;
139
+
140
+ const actionDisplay = storeName ? `${action} (${storeName})` : action;
141
+
142
+ return (
143
+ <ScrollView
144
+ style={styles.container}
145
+ contentContainerStyle={styles.contentContainer}
146
+ showsVerticalScrollIndicator={true}
147
+ scrollEventThrottle={16}
148
+ keyboardShouldPersistTaps='handled'>
149
+
150
+ <View style={styles.header}>
151
+ <View style={styles.headerInfo}>
152
+ <Text style={styles.actionIndicator}>
153
+ Action: <Text style={[styles.actionName, { color: actionColor }]}>{actionDisplay}</Text>
154
+ </Text>
155
+ <Text style={styles.timestamp}>
156
+ {timestamp
157
+ ? new Date(timestamp).toLocaleString()
158
+ : 'Unknown time'}
159
+ </Text>
160
+ {actionCompleteTime && (
161
+ <Text style={styles.actionTime}>
162
+ Completed in: {actionCompleteTime}ms
163
+ </Text>
164
+ )}
165
+ </View>
166
+ <CopyButton text={formattedLog} />
167
+ </View>
168
+
169
+ {hasChanges && (
170
+ <CollapsibleSection title='State Changes' initiallyExpanded={true}>
171
+ <View style={styles.dataContentWrapper}>
172
+ <View style={styles.dataContent}>
173
+ {stateChanges.map((change, index) => (
174
+ <View key={index} style={[styles.changeItem, index === stateChanges.length - 1 && styles.changeItemLast]}>
175
+ <Text style={styles.changePath}>{change.path}</Text>
176
+ <View style={styles.changeValues}>
177
+ <View style={styles.changeValue}>
178
+ <Text style={styles.changeLabel}>From:</Text>
179
+ <JSONTree
180
+ data={change.prevValue}
181
+ theme={theme}
182
+ invertTheme={true}
183
+ hideRoot={true}
184
+ shouldExpandNode={() => true}
185
+ />
186
+ </View>
187
+ <View style={styles.changeValue}>
188
+ <Text style={styles.changeLabel}>To:</Text>
189
+ <JSONTree
190
+ data={change.nextValue}
191
+ theme={theme}
192
+ invertTheme={true}
193
+ hideRoot={true}
194
+ shouldExpandNode={() => true}
195
+ />
196
+ </View>
197
+ </View>
198
+ </View>
199
+ ))}
200
+ </View>
201
+ </View>
202
+ </CollapsibleSection>
203
+ )}
204
+
205
+ <CollapsibleSection title='Previous State' initiallyExpanded={false}>
206
+ <View style={styles.dataContentWrapper}>
207
+ <View style={styles.dataContent}>
208
+ <JSONTree
209
+ data={prevState || {}}
210
+ theme={theme}
211
+ invertTheme={true}
212
+ hideRoot={true}
213
+ shouldExpandNode={(keyPath, data, level) => level < 1}
214
+ />
215
+ </View>
216
+ </View>
217
+ </CollapsibleSection>
218
+
219
+ <CollapsibleSection title='Next State' initiallyExpanded={true}>
220
+ <View style={styles.dataContentWrapper}>
221
+ <View style={styles.dataContent}>
222
+ <JSONTree
223
+ data={nextState || {}}
224
+ theme={theme}
225
+ invertTheme={true}
226
+ hideRoot={true}
227
+ shouldExpandNode={(keyPath, data, level) => level < 1}
228
+ />
229
+ </View>
230
+ </View>
231
+ </CollapsibleSection>
232
+
233
+ </ScrollView>
234
+ )
235
+ }
236
+
237
+ // Re-using and adapting styles from ConsoleLogDetails
238
+ const styles = StyleSheet.create({
239
+ container: {
240
+ flex: 1,
241
+ backgroundColor: '#fff',
242
+ },
243
+ contentContainer: {
244
+ paddingBottom: 20,
245
+ },
246
+ header: {
247
+ flexDirection: 'row',
248
+ padding: 15,
249
+ borderBottomWidth: 1,
250
+ borderBottomColor: '#eee',
251
+ alignItems: 'center',
252
+ justifyContent: 'space-between',
253
+ },
254
+ headerInfo: {
255
+ flexShrink: 1,
256
+ marginRight: 10,
257
+ },
258
+ actionIndicator: {
259
+ fontSize: 14,
260
+ marginBottom: 5,
261
+ },
262
+ actionName: {
263
+ fontWeight: 'bold',
264
+ },
265
+ timestamp: {
266
+ fontSize: 13,
267
+ color: '#666',
268
+ },
269
+ actionTime: {
270
+ fontSize: 13,
271
+ color: '#666',
272
+ marginTop: 2,
273
+ },
274
+ collapsibleSection: {
275
+ marginBottom: 1,
276
+ backgroundColor: '#fff',
277
+ },
278
+ sectionHeader: {
279
+ flexDirection: 'row',
280
+ justifyContent: 'space-between',
281
+ alignItems: 'center',
282
+ padding: 15,
283
+ backgroundColor: '#f5f5f5',
284
+ },
285
+ sectionTitle: {
286
+ fontSize: 15,
287
+ fontWeight: 'bold',
288
+ color: '#333',
289
+ },
290
+ expandIcon: {
291
+ fontSize: 14,
292
+ color: '#666',
293
+ },
294
+ dataContentWrapper: {
295
+ flex: 1,
296
+ padding: 10,
297
+ },
298
+ dataContent: {
299
+ backgroundColor: '#f8f9fa',
300
+ padding: 10,
301
+ borderRadius: 4,
302
+ borderWidth: 1,
303
+ borderColor: '#e9ecef',
304
+ },
305
+ errorContainer: {
306
+ flex: 1,
307
+ justifyContent: 'center',
308
+ alignItems: 'center',
309
+ padding: 20,
310
+ },
311
+ errorText: {
312
+ color: '#ff4444',
313
+ fontSize: 16,
314
+ },
315
+ copyButton: {
316
+ backgroundColor: '#e9ecef',
317
+ paddingHorizontal: 10,
318
+ paddingVertical: 5,
319
+ borderRadius: 4,
320
+ flexShrink: 0,
321
+ },
322
+ copyButtonText: {
323
+ fontSize: 12,
324
+ color: '#666',
325
+ },
326
+ changeItem: {
327
+ borderBottomWidth: 1,
328
+ borderBottomColor: '#eee',
329
+ paddingVertical: 8,
330
+ },
331
+ changeItemLast: {
332
+ borderBottomWidth: 0,
333
+ },
334
+ changePath: {
335
+ fontWeight: 'bold',
336
+ color: '#333',
337
+ marginBottom: 5,
338
+ fontFamily: 'monospace',
339
+ },
340
+ changeValues: {
341
+ flexDirection: 'row',
342
+ justifyContent: 'space-between',
343
+ },
344
+ changeValue: {
345
+ flex: 1,
346
+ padding: 5,
347
+ },
348
+ changeLabel: {
349
+ color: '#666',
350
+ fontSize: 12,
351
+ marginBottom: 3,
352
+ },
353
+ });
354
+
355
+ export default ZustandLogDetails
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-debug-toolkit",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "author": "zcj",
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "@react-native-async-storage/async-storage": "^2.1.2",
26
+ "react-native-mmkv": "2.12.2",
27
27
  "react-native-json-tree": "^1.5.0",
28
28
  "react-native-performance": "5.1.2",
29
29
  "react-native-root-siblings": "^4.0.0"