react-native-inapp-inspector 1.0.15 → 1.0.16

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.
@@ -1,5 +1,5 @@
1
- import React, { useState } from 'react';
2
- import { StyleSheet, Text, View, Pressable, Platform } from 'react-native';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, LayoutAnimation, Platform, Pressable, StyleSheet, Text, UIManager, View, } from 'react-native';
3
3
  import { AppColors } from '../styles/AppColors';
4
4
  import { AppFonts } from '../styles/AppFonts';
5
5
  import HighlightText from './HighlightText';
@@ -70,7 +70,10 @@ const getLogMessageWithBadges = (message, searchStr, textStyle, highlightStyle,
70
70
  const remainingText = message.substring(fullPrefix.length);
71
71
  const tags = fullPrefix.match(/\[[^\]]+\]/g) || [];
72
72
  const getTagColor = (tag) => {
73
- const cleanTag = tag.replace(/[\[\]]/g, '').trim().toUpperCase();
73
+ const cleanTag = tag
74
+ .replace(/[\[\]]/g, '')
75
+ .trim()
76
+ .toUpperCase();
74
77
  if (cleanTag === 'API')
75
78
  return '#0284C7';
76
79
  if (cleanTag === 'TEST')
@@ -87,7 +90,14 @@ const getLogMessageWithBadges = (message, searchStr, textStyle, highlightStyle,
87
90
  for (let i = 0; i < cleanTag.length; i++) {
88
91
  hash = cleanTag.charCodeAt(i) + ((hash << 5) - hash);
89
92
  }
90
- const colors = ['#0891B2', '#0D9488', '#2563EB', '#D97706', '#E11D48', '#8B5CF6'];
93
+ const colors = [
94
+ '#0891B2',
95
+ '#0D9488',
96
+ '#2563EB',
97
+ '#D97706',
98
+ '#E11D48',
99
+ '#8B5CF6',
100
+ ];
91
101
  return colors[Math.abs(hash) % colors.length];
92
102
  };
93
103
  return (<Text style={textStyle} numberOfLines={numberOfLines}>
@@ -108,6 +118,7 @@ const getLogMessageWithBadges = (message, searchStr, textStyle, highlightStyle,
108
118
  };
109
119
  export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchStr = '', isWebView = false, }) {
110
120
  const [expanded, setExpanded] = useState(false);
121
+ const chevronAnim = useRef(new Animated.Value(0)).current;
111
122
  const jsonContent = getJsonContent(item.message);
112
123
  const isAnalyticsError = item.message
113
124
  .toLowerCase()
@@ -191,6 +202,26 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
191
202
  }
192
203
  };
193
204
  const colors = getLogColors();
205
+ useEffect(() => {
206
+ if (Platform.OS === 'android') {
207
+ UIManager.setLayoutAnimationEnabledExperimental?.(true);
208
+ }
209
+ }, []);
210
+ useEffect(() => {
211
+ Animated.timing(chevronAnim, {
212
+ toValue: expanded ? 1 : 0,
213
+ duration: 180,
214
+ useNativeDriver: true,
215
+ }).start();
216
+ }, [chevronAnim, expanded]);
217
+ const toggleExpanded = () => {
218
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
219
+ setExpanded(prev => !prev);
220
+ };
221
+ const chevronRotate = chevronAnim.interpolate({
222
+ inputRange: [0, 1],
223
+ outputRange: ['0deg', '180deg'],
224
+ });
194
225
  // Show limited lines unless expanded
195
226
  const numLines = expanded ? undefined : 5;
196
227
  const hasLongMessage = jsonContent
@@ -211,10 +242,8 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
211
242
  paddingRight: 4,
212
243
  },
213
244
  ]}>
214
-
215
245
  {/* Left Content Area */}
216
- <Pressable onPress={() => setExpanded(prev => !prev)} style={{ flex: 1, paddingRight: 6 }}>
217
-
246
+ <Pressable onPress={toggleExpanded} style={{ flex: 1, paddingRight: 6 }}>
218
247
  <View style={styles.cardHeader}>
219
248
  <View style={styles.headerLeft}>
220
249
  <CopyButton value={item.message} label="Log message"/>
@@ -232,7 +261,10 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
232
261
  },
233
262
  ]}>
234
263
  <Text style={[styles.badgeText, { color: '#6B4EFF' }]}>
235
- console.{('sourceMethod' in item ? item.sourceMethod : undefined) || item.type || 'log'}
264
+ console.
265
+ {('sourceMethod' in item ? item.sourceMethod : undefined) ||
266
+ item.type ||
267
+ 'log'}
236
268
  </Text>
237
269
  </View>
238
270
  {jsonContent && (<View style={[
@@ -244,7 +276,9 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
244
276
  },
245
277
  ]}>
246
278
  <Text style={[styles.badgeText, { color: '#0D9488' }]}>
247
- {Array.isArray(jsonContent.data) ? `Array[${jsonContent.data.length}]` : `Object{${Object.keys(jsonContent.data).length}}`}
279
+ {Array.isArray(jsonContent.data)
280
+ ? `Array[${jsonContent.data.length}]`
281
+ : `Object{${Object.keys(jsonContent.data).length}}`}
248
282
  </Text>
249
283
  </View>)}
250
284
  {isAnalyticsError && (<View style={[
@@ -263,7 +297,10 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
263
297
  borderWidth: 1,
264
298
  },
265
299
  ]}>
266
- <Text style={[styles.badgeText, { color: AppColors.grayTextStrong }]}>
300
+ <Text style={[
301
+ styles.badgeText,
302
+ { color: AppColors.grayTextStrong },
303
+ ]}>
267
304
  user-log
268
305
  </Text>
269
306
  </View>)}
@@ -279,43 +316,92 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
279
316
  webview
280
317
  </Text>
281
318
  </View>)}
282
- <Text style={[styles.serialNumber, { color: AppColors.grayTextWeak }]}>#{item.id + 1}</Text>
283
- <Text style={[styles.timestamp, { color: AppColors.grayTextWeak }]}>{formatTime(item.timestamp)}</Text>
319
+ <Text style={[styles.serialNumber, { color: AppColors.grayTextWeak }]}>
320
+ #{item.id + 1}
321
+ </Text>
322
+ <Text style={[styles.timestamp, { color: AppColors.grayTextWeak }]}>
323
+ {formatTime(item.timestamp)}
324
+ </Text>
284
325
  </View>
285
-
286
- {caller && caller !== 'Unknown' && (<Text style={[styles.callerText, { color: AppColors.grayTextWeak, marginRight: 4 }]} numberOfLines={1} ellipsizeMode="middle">
326
+
327
+ {caller && caller !== 'Unknown' && (<Text style={[
328
+ styles.callerText,
329
+ { color: AppColors.grayTextWeak, marginRight: 4 },
330
+ ]} numberOfLines={1} ellipsizeMode="middle">
287
331
  {caller.split('/').pop() || caller}
288
332
  </Text>)}
289
333
  </View>
290
334
 
291
- <View style={[styles.cardBody, { backgroundColor: AppColors.primaryLight, borderColor: AppColors.dividerColor }]}>
335
+ <View style={[
336
+ styles.cardBody,
337
+ {
338
+ backgroundColor: AppColors.primaryLight,
339
+ borderColor: AppColors.dividerColor,
340
+ },
341
+ ]}>
292
342
  {jsonContent ? (<>
293
- {jsonContent.header ? (<Pressable onPress={() => setExpanded(prev => !prev)}>
343
+ {jsonContent.header ? (<Pressable onPress={toggleExpanded}>
294
344
  {getLogMessageWithBadges(jsonContent.header, searchStr, [styles.messageText, { color: AppColors.primaryBlack }], styles.highlight, numLines)}
295
345
  </Pressable>) : null}
296
- {expanded ? (<View style={[styles.jsonContainer, { backgroundColor: AppColors.grayBackground, borderColor: AppColors.dividerColor }]}>
346
+ {expanded ? (<View style={[
347
+ styles.jsonContainer,
348
+ {
349
+ backgroundColor: AppColors.grayBackground,
350
+ borderColor: AppColors.dividerColor,
351
+ },
352
+ ]}>
297
353
  <JsonViewer data={jsonContent.data} search={searchStr} forceOpen={expanded}/>
298
- </View>) : (<Pressable onPress={() => setExpanded(prev => !prev)} style={[styles.jsonPreviewContainer, { backgroundColor: AppColors.grayBackground, borderColor: AppColors.dividerColor }]}>
299
- <HighlightText text={getJsonPreviewText(jsonContent.data).text} search={searchStr} style={[styles.jsonPreviewText, { color: AppColors.primaryBlack }]} highlightStyle={styles.highlight} detectLinks={true}/>
354
+ </View>) : (<Pressable onPress={toggleExpanded} style={[
355
+ styles.jsonPreviewContainer,
356
+ {
357
+ backgroundColor: AppColors.grayBackground,
358
+ borderColor: AppColors.dividerColor,
359
+ },
360
+ ]}>
361
+ <HighlightText text={getJsonPreviewText(jsonContent.data).text} search={searchStr} style={[
362
+ styles.jsonPreviewText,
363
+ { color: AppColors.primaryBlack },
364
+ ]} highlightStyle={styles.highlight} detectLinks={true}/>
300
365
  </Pressable>)}
301
- </>) : (<Pressable onPress={() => setExpanded(prev => !prev)}>
366
+ </>) : (<Pressable onPress={toggleExpanded}>
302
367
  {getLogMessageWithBadges(item.message, searchStr, [styles.messageText, { color: AppColors.primaryBlack }], styles.highlight, numLines)}
303
368
  </Pressable>)}
304
- {hasLongMessage && (<Pressable onPress={() => setExpanded(prev => !prev)} style={styles.seeMoreBtn} hitSlop={8}>
369
+ {hasLongMessage && (<Pressable onPress={toggleExpanded} style={styles.seeMoreBtn} hitSlop={8}>
305
370
  <Text style={styles.seeMoreText}>
306
371
  {expanded ? 'See Less' : 'See More'}
307
372
  </Text>
308
373
  </Pressable>)}
309
374
  </View>
310
375
 
311
- {expanded && (<View style={[styles.cardFooter, { borderTopColor: AppColors.dividerColor, gap: 6 }]}>
312
- <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
313
- <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 10.5, color: AppColors.grayTextWeak }}>
314
- Length: {item.message.length} chars • Size: {encodeURIComponent(item.message).replace(/%[0-9A-F]{2}/g, 'a').length} bytes
376
+ {expanded && (<View style={[
377
+ styles.cardFooter,
378
+ { borderTopColor: AppColors.dividerColor, gap: 6 },
379
+ ]}>
380
+ <View style={{
381
+ flexDirection: 'row',
382
+ justifyContent: 'space-between',
383
+ alignItems: 'center',
384
+ }}>
385
+ <Text style={{
386
+ fontFamily: AppFonts.interRegular,
387
+ fontSize: 10.5,
388
+ color: AppColors.grayTextWeak,
389
+ }}>
390
+ Length: {item.message.length} chars • Size:{' '}
391
+ {encodeURIComponent(item.message).replace(/%[0-9A-F]{2}/g, 'a').length}{' '}
392
+ bytes
315
393
  </Text>
316
394
  </View>
317
- {caller && caller !== 'Unknown' && (<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginTop: 4 }}>
318
- <Text style={[styles.fullCallerText, { color: AppColors.grayText, flex: 1, marginRight: 8 }]} numberOfLines={2}>
395
+ {caller && caller !== 'Unknown' && (<View style={{
396
+ flexDirection: 'row',
397
+ alignItems: 'center',
398
+ justifyContent: 'space-between',
399
+ marginTop: 4,
400
+ }}>
401
+ <Text style={[
402
+ styles.fullCallerText,
403
+ { color: AppColors.grayText, flex: 1, marginRight: 8 },
404
+ ]} numberOfLines={2}>
319
405
  Caller: {caller}
320
406
  </Text>
321
407
  <CopyButton value={caller} label="Caller stack frame"/>
@@ -324,7 +410,7 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
324
410
  </Pressable>
325
411
 
326
412
  {/* Right Isolated Chevron Area */}
327
- <Pressable onPress={() => setExpanded(prev => !prev)} style={{
413
+ <Pressable onPress={toggleExpanded} style={{
328
414
  width: 28,
329
415
  alignItems: 'center',
330
416
  justifyContent: 'center',
@@ -332,11 +418,10 @@ export const ConsoleLogCard = React.memo(function ConsoleLogCard({ item, searchS
332
418
  marginTop: expanded ? 8 : 0,
333
419
  height: expanded ? 32 : undefined,
334
420
  }} hitSlop={12}>
335
- <View style={{ transform: [{ rotate: expanded ? '180deg' : '0deg' }] }}>
421
+ <Animated.View style={{ transform: [{ rotate: chevronRotate }] }}>
336
422
  <ChevronIcon size={16} color={AppColors.grayTextWeak}/>
337
- </View>
423
+ </Animated.View>
338
424
  </Pressable>
339
-
340
425
  </View>
341
426
  </View>);
342
427
  });
@@ -1,13 +1,31 @@
1
- import React from 'react';
2
- import { DevSettings, Alert, View, Text } from 'react-native';
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, DevSettings, Alert, Text } from 'react-native';
3
3
  // Components
4
4
  import TouchableScale from './TouchableScale';
5
+ import AnimatedEntrance from './AnimatedEntrance';
5
6
  // Assets
6
7
  import { EmptyRadarIcon } from './NetworkIcons';
7
8
  // Stylesheet
8
9
  import { AppColors } from '../styles/AppColors';
9
10
  import styles from '../styles';
10
11
  const EmptyState = ({ isSearch }) => {
12
+ const iconPulse = useRef(new Animated.Value(1)).current;
13
+ useEffect(() => {
14
+ const loop = Animated.loop(Animated.sequence([
15
+ Animated.timing(iconPulse, {
16
+ toValue: 1.06,
17
+ duration: 900,
18
+ useNativeDriver: true,
19
+ }),
20
+ Animated.timing(iconPulse, {
21
+ toValue: 1,
22
+ duration: 900,
23
+ useNativeDriver: true,
24
+ }),
25
+ ]));
26
+ loop.start();
27
+ return () => loop.stop();
28
+ }, [iconPulse]);
11
29
  const handleReload = () => {
12
30
  if (__DEV__ && DevSettings && DevSettings.reload) {
13
31
  DevSettings.reload();
@@ -15,10 +33,10 @@ const EmptyState = ({ isSearch }) => {
15
33
  }
16
34
  Alert.alert('Reload', 'App reload is typically only available in development mode.');
17
35
  };
18
- return (<View style={styles.emptyContainer}>
19
- <View style={styles.emptyIconWrap}>
36
+ return (<AnimatedEntrance style={styles.emptyContainer} distance={14}>
37
+ <Animated.View style={[styles.emptyIconWrap, { transform: [{ scale: iconPulse }] }]}>
20
38
  <EmptyRadarIcon color={AppColors.purple} size={32}/>
21
- </View>
39
+ </Animated.View>
22
40
  <Text style={styles.emptyTitle}>
23
41
  {isSearch ? 'No matching APIs' : 'No network activity'}
24
42
  </Text>
@@ -30,6 +48,6 @@ const EmptyState = ({ isSearch }) => {
30
48
  {!isSearch && (<TouchableScale style={styles.reloadBtn} onPress={handleReload}>
31
49
  <Text style={styles.reloadBtnText}>Reload App</Text>
32
50
  </TouchableScale>)}
33
- </View>);
51
+ </AnimatedEntrance>);
34
52
  };
35
53
  export default EmptyState;
@@ -1,9 +1,10 @@
1
- import React, { useState } from 'react';
2
- import { View, Text, Pressable, StyleSheet } from 'react-native';
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { Animated, LayoutAnimation, Platform, Pressable, StyleSheet, Text, UIManager, View, } from 'react-native';
3
3
  import { AppColors } from '../styles/AppColors';
4
4
  import { AppFonts } from '../styles/AppFonts';
5
5
  import { ChevronIcon } from './NetworkIcons';
6
6
  import Svg, { Path } from 'react-native-svg';
7
+ import AnimatedEntrance from './AnimatedEntrance';
7
8
  // Custom icons
8
9
  const DatabaseIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
9
10
  <Path d="M12 2C6.5 2 2 4.2 2 7v10c0 2.8 4.5 5 10 5s10-2.2 10-5V7c0-2.8-4.5-5-10-5z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
@@ -16,6 +17,26 @@ const BoltIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width=
16
17
  const FolderIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
17
18
  <Path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
18
19
  </Svg>);
20
+ const animateTreeLayout = () => {
21
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
22
+ };
23
+ const AnimatedChevron = ({ color, expanded, size, style, }) => {
24
+ const progress = useRef(new Animated.Value(expanded ? 1 : 0)).current;
25
+ useEffect(() => {
26
+ Animated.timing(progress, {
27
+ toValue: expanded ? 1 : 0,
28
+ duration: 180,
29
+ useNativeDriver: true,
30
+ }).start();
31
+ }, [expanded, progress]);
32
+ const rotate = progress.interpolate({
33
+ inputRange: [0, 1],
34
+ outputRange: ['0deg', '90deg'],
35
+ });
36
+ return (<Animated.View style={[style, { transform: [{ rotate }] }]}>
37
+ <ChevronIcon color={color} size={size}/>
38
+ </Animated.View>);
39
+ };
19
40
  const ReduxValueNode = ({ name, value, level, search }) => {
20
41
  const [expanded, setExpanded] = useState(level < 1);
21
42
  const isObject = typeof value === 'object' && value !== null;
@@ -39,7 +60,11 @@ const ReduxValueNode = ({ name, value, level, search }) => {
39
60
  return null;
40
61
  }
41
62
  if (!isObject) {
42
- const valStr = value === null ? 'null' : value === undefined ? 'undefined' : String(value);
63
+ const valStr = value === null
64
+ ? 'null'
65
+ : value === undefined
66
+ ? 'undefined'
67
+ : String(value);
43
68
  // Pick different colors for primitives
44
69
  let valColor = '#0D9488'; // String
45
70
  if (value === null || value === undefined) {
@@ -63,13 +88,16 @@ const ReduxValueNode = ({ name, value, level, search }) => {
63
88
  </View>);
64
89
  }
65
90
  const keys = Object.keys(value);
66
- const summaryText = isArray ? `Array [${keys.length}]` : `Object {${keys.length}}`;
91
+ const summaryText = isArray
92
+ ? `Array [${keys.length}]`
93
+ : `Object {${keys.length}}`;
67
94
  return (<View style={reduxValueStyles.treeNodeBlock}>
68
- <Pressable onPress={() => setExpanded(!expanded)} style={reduxValueStyles.treeRow}>
95
+ <Pressable onPress={() => {
96
+ animateTreeLayout();
97
+ setExpanded(!expanded);
98
+ }} style={reduxValueStyles.treeRow}>
69
99
  <View style={reduxValueStyles.treeLeafConnector}/>
70
- <View style={[reduxValueStyles.chevronWrap, { transform: [{ rotate: expanded ? '90deg' : '0deg' }] }]}>
71
- <ChevronIcon color={AppColors.grayTextWeak} size={10}/>
72
- </View>
100
+ <AnimatedChevron color={AppColors.grayTextWeak} expanded={expanded} size={10} style={reduxValueStyles.chevronWrap}/>
73
101
  <Text style={reduxValueStyles.keyText} selectable={true}>
74
102
  {nameStr}
75
103
  <Text style={reduxValueStyles.colonText}>: </Text>
@@ -84,13 +112,24 @@ const ReduxValueNode = ({ name, value, level, search }) => {
84
112
  export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
85
113
  const [storeExpanded, setStoreExpanded] = useState(true);
86
114
  const [reducerExpanded, setReducerExpanded] = useState({});
115
+ useEffect(() => {
116
+ if (Platform.OS === 'android') {
117
+ UIManager.setLayoutAnimationEnabledExperimental?.(true);
118
+ }
119
+ }, []);
87
120
  if (!state || typeof state !== 'object') {
88
- return (<Text style={{ fontFamily: AppFonts.interRegular, fontSize: 12, color: AppColors.grayTextWeak, padding: 12 }}>
121
+ return (<Text style={{
122
+ fontFamily: AppFonts.interRegular,
123
+ fontSize: 12,
124
+ color: AppColors.grayTextWeak,
125
+ padding: 12,
126
+ }}>
89
127
  No state object to display.
90
128
  </Text>);
91
129
  }
92
130
  const reducers = Object.keys(state);
93
131
  const toggleReducer = (key) => {
132
+ animateTreeLayout();
94
133
  setReducerExpanded(prev => ({
95
134
  ...prev,
96
135
  [key]: !prev[key],
@@ -98,10 +137,11 @@ export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
98
137
  };
99
138
  return (<View style={styles.container}>
100
139
  {/* Root Node: Store */}
101
- <Pressable onPress={() => setStoreExpanded(!storeExpanded)} style={styles.storeHeader}>
102
- <View style={[styles.chevronWrap, { transform: [{ rotate: storeExpanded ? '90deg' : '0deg' }] }]}>
103
- <ChevronIcon color="#FFFFFF" size={12}/>
104
- </View>
140
+ <Pressable onPress={() => {
141
+ animateTreeLayout();
142
+ setStoreExpanded(!storeExpanded);
143
+ }} style={styles.storeHeader}>
144
+ <AnimatedChevron color="#FFFFFF" expanded={storeExpanded} size={12} style={styles.chevronWrap}/>
105
145
  <Text style={styles.storeTitle}>🏪 Redux Store</Text>
106
146
  <View style={styles.badge}>
107
147
  <Text style={styles.badgeText}>{reducers.length} Reducers</Text>
@@ -116,14 +156,15 @@ export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
116
156
  const lastAction = lastActionMap[reducerKey];
117
157
  return (<View key={reducerKey} style={styles.reducerContainer}>
118
158
  {/* Visual Branch Line for Reducer */}
119
- <View style={[styles.reducerVerticalLine, isLastReducer && { bottom: '50%' }]}/>
159
+ <View style={[
160
+ styles.reducerVerticalLine,
161
+ isLastReducer && { bottom: '50%' },
162
+ ]}/>
120
163
 
121
164
  {/* Reducer Header */}
122
165
  <Pressable onPress={() => toggleReducer(reducerKey)} style={styles.reducerHeader}>
123
166
  <View style={styles.reducerHorizontalLine}/>
124
- <View style={[styles.chevronWrap, { transform: [{ rotate: isExpanded ? '90deg' : '0deg' }] }]}>
125
- <ChevronIcon color={AppColors.purple} size={10}/>
126
- </View>
167
+ <AnimatedChevron color={AppColors.purple} expanded={isExpanded} size={10} style={styles.chevronWrap}/>
127
168
  <View style={styles.iconWrap}>
128
169
  <FolderIcon color={AppColors.purple} size={11}/>
129
170
  </View>
@@ -141,13 +182,24 @@ export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
141
182
  <BoltIcon color="#DB2777" size={11}/>
142
183
  </View>
143
184
  <View style={{ flex: 1 }}>
144
- <View style={{ flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', gap: 6 }}>
185
+ <View style={{
186
+ flexDirection: 'row',
187
+ alignItems: 'center',
188
+ flexWrap: 'wrap',
189
+ gap: 6,
190
+ }}>
145
191
  <Text style={styles.childLabel}>Last Action:</Text>
146
192
  {lastAction ? (<View style={styles.actionTypeBadge}>
147
- <Text style={styles.actionTypeText}>{lastAction.type}</Text>
148
- </View>) : (<Text style={styles.noActionText}>None dispatched</Text>)}
193
+ <Text style={styles.actionTypeText}>
194
+ {lastAction.type}
195
+ </Text>
196
+ </View>) : (<Text style={styles.noActionText}>
197
+ None dispatched
198
+ </Text>)}
149
199
  </View>
150
- {lastAction && (<Text style={styles.timestampText}>Dispatched: {lastAction.timestamp}</Text>)}
200
+ {lastAction && (<Text style={styles.timestampText}>
201
+ Dispatched: {lastAction.timestamp}
202
+ </Text>)}
151
203
  {lastAction && lastAction.payload !== null && (<View style={{ marginTop: 6 }}>
152
204
  <ReduxValueNode name="payload" value={lastAction.payload} level={0} search={search}/>
153
205
  </View>)}
@@ -176,6 +228,7 @@ export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
176
228
  export const ReduxActionTimeline = ({ history, onClear, search, }) => {
177
229
  const [expandedActionId, setExpandedActionId] = useState(null);
178
230
  const toggleExpand = (id) => {
231
+ animateTreeLayout();
179
232
  setExpandedActionId(prev => (prev === id ? null : id));
180
233
  };
181
234
  const filteredHistory = history.filter(action => {
@@ -193,7 +246,9 @@ export const ReduxActionTimeline = ({ history, onClear, search, }) => {
193
246
  });
194
247
  return (<View style={timelineStyles.container}>
195
248
  <View style={timelineStyles.headerRow}>
196
- <Text style={timelineStyles.headerTitle}>⚡ Dispatched Actions ({filteredHistory.length})</Text>
249
+ <Text style={timelineStyles.headerTitle}>
250
+ ⚡ Dispatched Actions ({filteredHistory.length})
251
+ </Text>
197
252
  {history.length > 0 && (<Pressable onPress={onClear} style={timelineStyles.clearBtn}>
198
253
  <Text style={timelineStyles.clearBtnText}>Clear Log</Text>
199
254
  </Pressable>)}
@@ -209,9 +264,12 @@ export const ReduxActionTimeline = ({ history, onClear, search, }) => {
209
264
  {filteredHistory.map((item, index) => {
210
265
  const isLast = index === filteredHistory.length - 1;
211
266
  const isExpanded = expandedActionId === item.id;
212
- return (<View key={item.id} style={timelineStyles.timelineItem}>
267
+ return (<AnimatedEntrance key={item.id} index={index} distance={8} style={timelineStyles.timelineItem}>
213
268
  {/* Visual Line */}
214
- <View style={[timelineStyles.verticalLine, isLast && { bottom: '50%' }]}/>
269
+ <View style={[
270
+ timelineStyles.verticalLine,
271
+ isLast && { bottom: '50%' },
272
+ ]}/>
215
273
  <View style={timelineStyles.circleIndicator}>
216
274
  <View style={timelineStyles.circleInner}/>
217
275
  </View>
@@ -219,13 +277,18 @@ export const ReduxActionTimeline = ({ history, onClear, search, }) => {
219
277
  {/* Card */}
220
278
  <Pressable onPress={() => toggleExpand(item.id)} style={[
221
279
  timelineStyles.card,
222
- isExpanded && { borderColor: AppColors.purple, backgroundColor: AppColors.purpleShade50 },
280
+ isExpanded && {
281
+ borderColor: AppColors.purple,
282
+ backgroundColor: AppColors.purpleShade50,
283
+ },
223
284
  ]}>
224
285
  <View style={timelineStyles.cardHeader}>
225
286
  <View style={timelineStyles.typeBadge}>
226
287
  <Text style={timelineStyles.typeText}>{item.type}</Text>
227
288
  </View>
228
- <Text style={timelineStyles.timestamp}>{item.timestamp}</Text>
289
+ <Text style={timelineStyles.timestamp}>
290
+ {item.timestamp}
291
+ </Text>
229
292
  </View>
230
293
 
231
294
  {item.affectedSlices.length > 0 && (<View style={timelineStyles.slicesRow}>
@@ -237,12 +300,15 @@ export const ReduxActionTimeline = ({ history, onClear, search, }) => {
237
300
 
238
301
  {isExpanded && (<View style={timelineStyles.payloadContainer}>
239
302
  <Text style={timelineStyles.payloadTitle}>Payload</Text>
240
- {item.payload !== null && typeof item.payload === 'object' ? (<ReduxValueNode name="action.payload" value={item.payload} level={0} search={search}/>) : (<Text style={timelineStyles.primitivePayload}>
241
- {item.payload === null ? 'null' : String(item.payload)}
303
+ {item.payload !== null &&
304
+ typeof item.payload === 'object' ? (<ReduxValueNode name="action.payload" value={item.payload} level={0} search={search}/>) : (<Text style={timelineStyles.primitivePayload}>
305
+ {item.payload === null
306
+ ? 'null'
307
+ : String(item.payload)}
242
308
  </Text>)}
243
309
  </View>)}
244
310
  </Pressable>
245
- </View>);
311
+ </AnimatedEntrance>);
246
312
  })}
247
313
  </View>)}
248
314
  </View>);
@@ -2,6 +2,22 @@ import React, { useRef } from 'react';
2
2
  import { Animated, Pressable, StyleSheet } from 'react-native';
3
3
  const TouchableScale = ({ onPress, style, children, hitSlop, disabled, }) => {
4
4
  const scale = useRef(new Animated.Value(1)).current;
5
+ const opacity = useRef(new Animated.Value(1)).current;
6
+ const animatePress = (pressed) => {
7
+ Animated.parallel([
8
+ Animated.spring(scale, {
9
+ toValue: pressed ? 0.94 : 1,
10
+ friction: 6,
11
+ tension: 120,
12
+ useNativeDriver: true,
13
+ }),
14
+ Animated.timing(opacity, {
15
+ toValue: pressed ? 0.86 : 1,
16
+ duration: pressed ? 90 : 140,
17
+ useNativeDriver: true,
18
+ }),
19
+ ]).start();
20
+ };
5
21
  const flattenedStyle = StyleSheet.flatten(style) || {};
6
22
  const layoutStyle = {
7
23
  flex: flattenedStyle.flex,
@@ -14,8 +30,8 @@ const TouchableScale = ({ onPress, style, children, hitSlop, disabled, }) => {
14
30
  flexShrink: flattenedStyle.flexShrink,
15
31
  gap: flattenedStyle.gap,
16
32
  };
17
- return (<Pressable disabled={disabled} style={style} onPressIn={() => Animated.spring(scale, { toValue: 0.94, useNativeDriver: true }).start()} onPressOut={() => Animated.spring(scale, { toValue: 1, useNativeDriver: true }).start()} onPress={onPress} hitSlop={hitSlop}>
18
- <Animated.View style={[{ transform: [{ scale }] }, layoutStyle]}>
33
+ return (<Pressable disabled={disabled} style={style} onPressIn={() => animatePress(true)} onPressOut={() => animatePress(false)} onPress={onPress} hitSlop={hitSlop}>
34
+ <Animated.View style={[{ opacity, transform: [{ scale }] }, layoutStyle]}>
19
35
  {children}
20
36
  </Animated.View>
21
37
  </Pressable>);