react-native-inapp-inspector 1.1.32 → 1.1.34

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.
@@ -14,13 +14,13 @@ import { getSize, openInVSCode, parseStackLine } from '../../helpers';
14
14
  import { getCustomStorage } from '../../helpers/settingsStore';
15
15
  import { AppColors } from '../../styles/AppColors';
16
16
  import { AppFonts } from '../../styles/AppFonts';
17
- import { SearchIcon, ClearIcon, ClockIcon, LayersIcon, TrashIcon, ForwardChevronIcon, LiveStateIcon, TimelineIcon, StorageIcon, MetadataIcon, BoltIcon, ExternalLinkIcon, } from '../NetworkIcons';
17
+ import { TerminalIcon, SearchIcon, ClearIcon, ClockIcon, LayersIcon, TrashIcon, ForwardChevronIcon, LiveStateIcon, TimelineIcon, StorageIcon, MetadataIcon, BoltIcon, AtomIcon, ScreenIcon, ListenerIcon, DocIcon, ExternalLinkIcon, CodeBracketsIcon, } from '../NetworkIcons';
18
18
  const getOriginBadge = (originType) => {
19
19
  switch (originType) {
20
20
  case 'saga':
21
21
  return {
22
22
  label: 'SAGA',
23
- icon: '⚡',
23
+ renderIcon: (color, size = 9) => (<BoltIcon color={color} size={size}/>),
24
24
  bg: AppColors.purple100,
25
25
  text: AppColors.brandPurple,
26
26
  border: AppColors.purple200,
@@ -28,7 +28,7 @@ const getOriginBadge = (originType) => {
28
28
  case 'thunk':
29
29
  return {
30
30
  label: 'THUNK',
31
- icon: '⚛️',
31
+ renderIcon: (color, size = 9) => (<AtomIcon color={color} size={size}/>),
32
32
  bg: AppColors.amber100,
33
33
  text: AppColors.amber800Warm,
34
34
  border: AppColors.amber200,
@@ -36,7 +36,7 @@ const getOriginBadge = (originType) => {
36
36
  case 'ui':
37
37
  return {
38
38
  label: 'UI',
39
- icon: '📱',
39
+ renderIcon: (color, size = 9) => (<ScreenIcon color={color} size={size}/>),
40
40
  bg: AppColors.sky100,
41
41
  text: AppColors.sky600,
42
42
  border: AppColors.sky400,
@@ -44,7 +44,7 @@ const getOriginBadge = (originType) => {
44
44
  case 'listener':
45
45
  return {
46
46
  label: 'LISTENER',
47
- icon: '👂',
47
+ renderIcon: (color, size = 9) => (<ListenerIcon color={color} size={size}/>),
48
48
  bg: AppColors.teal100,
49
49
  text: AppColors.teal700,
50
50
  border: AppColors.teal400,
@@ -52,7 +52,7 @@ const getOriginBadge = (originType) => {
52
52
  default:
53
53
  return {
54
54
  label: 'DIRECT',
55
- icon: '⚡',
55
+ renderIcon: (color, size = 9) => (<BoltIcon color={color} size={size}/>),
56
56
  bg: AppColors.slate100,
57
57
  text: AppColors.slate700,
58
58
  border: AppColors.slate200,
@@ -61,11 +61,12 @@ const getOriginBadge = (originType) => {
61
61
  };
62
62
  const ReduxDetail = React.memo(() => {
63
63
  const { t } = useTranslation();
64
- const { reduxState, reduxLastActionMap, selectedReduxSlice, selectedReduxAction, setSelectedReduxSlice, setSelectedReduxAction, } = useInspector();
64
+ const { reduxState, reduxLastActionMap, selectedReduxSlice, selectedReduxAction, setSelectedReduxSlice, } = useInspector();
65
65
  // Search filter within details
66
66
  const [detailSearch, setDetailSearch] = useState('');
67
- // Sub-tabs for Slice
67
+ // Sub-tabs for Slice & Action
68
68
  const [sliceTab, setSliceTab] = useState('live');
69
+ const [actionTab, setActionTab] = useState('payload');
69
70
  const [viewMode, setViewMode] = useState('pretty');
70
71
  // Selected Action inside Timeline
71
72
  const [expandedActionId, setExpandedActionId] = useState(null);
@@ -86,6 +87,18 @@ const ReduxDetail = React.memo(() => {
86
87
  return action.affectedSlices.includes(selectedReduxSlice);
87
88
  });
88
89
  }, [allActionHistory, selectedReduxSlice]);
90
+ // Filtered slice actions by search query in timeline tab
91
+ const filteredTimelineActions = useMemo(() => {
92
+ if (!detailSearch.trim())
93
+ return sliceActions;
94
+ const q = detailSearch.toLowerCase();
95
+ return sliceActions.filter(action => {
96
+ return (action.type.toLowerCase().includes(q) ||
97
+ (action.callerFile && action.callerFile.toLowerCase().includes(q)) ||
98
+ (action.payload &&
99
+ JSON.stringify(action.payload).toLowerCase().includes(q)));
100
+ });
101
+ }, [sliceActions, detailSearch]);
89
102
  useEffect(() => {
90
103
  if (!selectedReduxSlice)
91
104
  return;
@@ -182,22 +195,24 @@ const ReduxDetail = React.memo(() => {
182
195
  const sliceTabs = [
183
196
  {
184
197
  key: 'live',
185
- label: t('redux.liveState'),
198
+ label: t('redux.liveState', 'Live State'),
186
199
  icon: (isActive) => (<LiveStateIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
187
200
  },
188
201
  {
189
202
  key: 'timeline',
190
- label: `${t('redux.timeline')} (${sliceActions.length})`,
203
+ label: `${t('redux.timeline', 'Timeline')} (${sliceActions.length})`,
191
204
  icon: (isActive) => (<TimelineIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
192
205
  },
193
206
  {
194
207
  key: 'persisted',
195
- label: isPersisted ? t('redux.persisted') : t('redux.storage'),
208
+ label: isPersisted
209
+ ? t('redux.persisted', 'Persisted')
210
+ : t('redux.storage', 'Storage'),
196
211
  icon: (isActive) => (<StorageIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
197
212
  },
198
213
  {
199
214
  key: 'metadata',
200
- label: t('redux.metadata'),
215
+ label: t('redux.metadata', 'Metadata'),
201
216
  icon: (isActive) => (<MetadataIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
202
217
  },
203
218
  ];
@@ -205,18 +220,34 @@ const ReduxDetail = React.memo(() => {
205
220
  {/* Info Header Bar */}
206
221
  <View style={reduxDetailStyles.infoBar}>
207
222
  <View style={reduxDetailStyles.infoTopRow}>
208
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap', flex: 1 }}>
209
- <View style={[reduxDetailStyles.methodBadge, { backgroundColor: AppColors.indigo600Alt }]}>
210
- <Text style={reduxDetailStyles.methodBadgeText}>{t('redux.slice')}</Text>
223
+ <View style={{
224
+ flexDirection: 'row',
225
+ alignItems: 'center',
226
+ gap: 6,
227
+ flexWrap: 'wrap',
228
+ flex: 1,
229
+ minWidth: 0,
230
+ }}>
231
+ <View style={[
232
+ reduxDetailStyles.methodBadge,
233
+ { backgroundColor: AppColors.indigo600Alt },
234
+ ]}>
235
+ <Text style={reduxDetailStyles.methodBadgeText}>
236
+ {t('redux.slice', 'SLICE')}
237
+ </Text>
211
238
  </View>
212
- <Text style={reduxDetailStyles.sliceNameText}>
239
+ <Text style={reduxDetailStyles.sliceNameText} numberOfLines={1}>
213
240
  {selectedReduxSlice}
214
241
  </Text>
215
242
  {isPersisted ? (<View style={reduxDetailStyles.persistedChip}>
216
243
  <StorageIcon color={AppColors.emerald700} size={10}/>
217
- <Text style={reduxDetailStyles.persistedChipText}>{t('redux.persisted').toUpperCase()}</Text>
244
+ <Text style={reduxDetailStyles.persistedChipText}>
245
+ {t('redux.persisted', 'Persisted').toUpperCase()}
246
+ </Text>
218
247
  </View>) : (<View style={reduxDetailStyles.inMemoryChip}>
219
- <Text style={reduxDetailStyles.inMemoryChipText}>{t('redux.inMemory').toUpperCase()}</Text>
248
+ <Text style={reduxDetailStyles.inMemoryChipText}>
249
+ {t('redux.inMemory', 'In-Memory').toUpperCase()}
250
+ </Text>
220
251
  </View>)}
221
252
  {lastAction?.timestamp && (<View style={reduxDetailStyles.timePill}>
222
253
  <ClockIcon color={AppColors.violet600} size={10}/>
@@ -227,29 +258,93 @@ const ReduxDetail = React.memo(() => {
227
258
  </View>
228
259
 
229
260
  {/* Copy Button */}
230
- <CopyButton value={() => (sliceTab === 'live' ? sliceData : persistedData)} label={t('redux.sliceJson')}/>
261
+ <CopyButton value={() => (sliceTab === 'live' ? sliceData : persistedData)} label={t('redux.sliceJson', 'Slice JSON')}/>
231
262
  </View>
232
263
 
233
264
  {/* Sub Stats Row */}
234
265
  <View style={reduxDetailStyles.subStatsRow}>
235
- <View style={[reduxDetailStyles.statPill, { backgroundColor: AppColors.indigo50, borderColor: AppColors.indigo400 }]}>
266
+ <View style={[
267
+ reduxDetailStyles.statPill,
268
+ {
269
+ backgroundColor: AppColors.indigo50,
270
+ borderColor: AppColors.indigo400,
271
+ },
272
+ ]}>
236
273
  <LayersIcon color={AppColors.indigo600Alt} size={11}/>
237
- <Text style={[reduxDetailStyles.statLabel, { color: AppColors.indigo600Alt }]}>{t('redux.keys')}:</Text>
238
- <Text style={[reduxDetailStyles.statValue, { color: AppColors.indigo600Alt }]}>{fieldsCount}</Text>
274
+ <Text style={[
275
+ reduxDetailStyles.statLabel,
276
+ { color: AppColors.indigo600Alt },
277
+ ]}>
278
+ {t('redux.keys', 'Keys')}:
279
+ </Text>
280
+ <Text style={[
281
+ reduxDetailStyles.statValue,
282
+ { color: AppColors.indigo600Alt },
283
+ ]}>
284
+ {fieldsCount}
285
+ </Text>
239
286
  </View>
240
- <View style={[reduxDetailStyles.statPill, { backgroundColor: AppColors.sky100, borderColor: AppColors.sky400 }]}>
241
- <Text style={[reduxDetailStyles.statLabel, { color: AppColors.sky600 }]}>{t('redux.size')}:</Text>
242
- <Text style={[reduxDetailStyles.statValue, { color: AppColors.sky600 }]}>{getSize(sliceData)}</Text>
287
+ <View style={[
288
+ reduxDetailStyles.statPill,
289
+ {
290
+ backgroundColor: AppColors.sky100,
291
+ borderColor: AppColors.sky400,
292
+ },
293
+ ]}>
294
+ <Text style={[
295
+ reduxDetailStyles.statLabel,
296
+ { color: AppColors.sky600 },
297
+ ]}>
298
+ {t('redux.size', 'Size')}:
299
+ </Text>
300
+ <Text style={[
301
+ reduxDetailStyles.statValue,
302
+ { color: AppColors.sky600 },
303
+ ]}>
304
+ {getSize(sliceData)}
305
+ </Text>
243
306
  </View>
244
- <View style={[reduxDetailStyles.statPill, { backgroundColor: AppColors.purple100, borderColor: AppColors.purple200 }]}>
307
+ <View style={[
308
+ reduxDetailStyles.statPill,
309
+ {
310
+ backgroundColor: AppColors.purple100,
311
+ borderColor: AppColors.purple200,
312
+ },
313
+ ]}>
245
314
  <TimelineIcon color={AppColors.violet600} size={11}/>
246
- <Text style={[reduxDetailStyles.statLabel, { color: AppColors.brandPurple }]}>{t('redux.timeline')}:</Text>
247
- <Text style={[reduxDetailStyles.statValue, { color: AppColors.brandPurple }]}>{sliceActions.length}</Text>
315
+ <Text style={[
316
+ reduxDetailStyles.statLabel,
317
+ { color: AppColors.brandPurple },
318
+ ]}>
319
+ {t('redux.timeline', 'Timeline')}:
320
+ </Text>
321
+ <Text style={[
322
+ reduxDetailStyles.statValue,
323
+ { color: AppColors.brandPurple },
324
+ ]}>
325
+ {sliceActions.length}
326
+ </Text>
248
327
  </View>
249
- {lastAction && (<View style={[reduxDetailStyles.statPill, { backgroundColor: AppColors.amber100, borderColor: AppColors.amber200, flex: 1, minWidth: 140 }]}>
328
+ {lastAction && (<View style={[
329
+ reduxDetailStyles.statPill,
330
+ {
331
+ backgroundColor: AppColors.amber100,
332
+ borderColor: AppColors.amber200,
333
+ flexShrink: 1,
334
+ maxWidth: 160,
335
+ },
336
+ ]}>
250
337
  <BoltIcon color={AppColors.amber800Warm} size={11}/>
251
- <Text style={[reduxDetailStyles.statLabel, { color: AppColors.amber800Warm }]}>{t('redux.last')}:</Text>
252
- <Text style={[reduxDetailStyles.statValue, { color: AppColors.amber800Warm }]} numberOfLines={1}>
338
+ <Text style={[
339
+ reduxDetailStyles.statLabel,
340
+ { color: AppColors.amber800Warm },
341
+ ]}>
342
+ {t('redux.last', 'Last')}:
343
+ </Text>
344
+ <Text style={[
345
+ reduxDetailStyles.statValue,
346
+ { color: AppColors.amber800Warm, flexShrink: 1 },
347
+ ]} numberOfLines={1}>
253
348
  {lastAction.type}
254
349
  </Text>
255
350
  </View>)}
@@ -259,10 +354,12 @@ const ReduxDetail = React.memo(() => {
259
354
  <SegmentedTabs tabs={sliceTabs} activeKey={sliceTab} onChange={key => setSliceTab(key)} style={{ marginTop: 10 }}/>
260
355
  </View>
261
356
 
262
- {/* Search Bar within slice */}
357
+ {/* Search Bar within slice (for Live, Timeline, Persisted) */}
263
358
  {sliceTab !== 'metadata' && (<View style={reduxDetailStyles.searchContainer}>
264
359
  <SearchIcon color={AppColors.grayTextWeak} size={14}/>
265
- <TextInput placeholder="Search keys, values, or action types..." placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={reduxDetailStyles.searchInput} autoCorrect={false} autoCapitalize="none"/>
360
+ <TextInput placeholder={sliceTab === 'timeline'
361
+ ? 'Search action type, file, or payload...'
362
+ : 'Search keys or values...'} placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={reduxDetailStyles.searchInput} autoCorrect={false} autoCapitalize="none"/>
266
363
  {detailSearch.length > 0 && (<Pressable onPress={() => setDetailSearch('')} hitSlop={10}>
267
364
  <ClearIcon color={AppColors.grayTextWeak} size={14}/>
268
365
  </Pressable>)}
@@ -270,11 +367,14 @@ const ReduxDetail = React.memo(() => {
270
367
 
271
368
  {/* Content Body */}
272
369
  {sliceTab === 'timeline' ? (<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 12, paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
273
- {sliceActions.length === 0 ? (<View style={reduxDetailStyles.emptyBox}>
370
+ {filteredTimelineActions.length === 0 ? (<View style={reduxDetailStyles.emptyBox}>
371
+ <TimelineIcon color={AppColors.grayTextWeak} size={28}/>
274
372
  <Text style={reduxDetailStyles.emptyBoxText}>
275
- No dispatched actions recorded for this slice yet.
373
+ {detailSearch.trim()
374
+ ? 'No actions matching your search query.'
375
+ : 'No dispatched actions recorded for this slice yet.'}
276
376
  </Text>
277
- </View>) : (sliceActions.map((action, aIdx) => {
377
+ </View>) : (filteredTimelineActions.map((action, aIdx) => {
278
378
  const isExpanded = expandedActionId === action.id;
279
379
  const originMeta = getOriginBadge(action.originType);
280
380
  const callerBasename = action.callerFile
@@ -283,7 +383,14 @@ const ReduxDetail = React.memo(() => {
283
383
  return (<AnimatedEntrance key={action.id} index={aIdx} distance={6}>
284
384
  <TouchableScale onPress={() => setExpandedActionId(isExpanded ? null : action.id)} style={reduxDetailStyles.timelineCard}>
285
385
  <View style={reduxDetailStyles.timelineHeader}>
286
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flex: 1, minWidth: 0, flexWrap: 'wrap' }}>
386
+ <View style={{
387
+ flexDirection: 'row',
388
+ alignItems: 'center',
389
+ gap: 6,
390
+ flex: 1,
391
+ minWidth: 0,
392
+ flexWrap: 'wrap',
393
+ }}>
287
394
  <View style={reduxDetailStyles.actionBadge}>
288
395
  <Text style={reduxDetailStyles.actionBadgeText}>
289
396
  #{action.id}
@@ -295,13 +402,13 @@ const ReduxDetail = React.memo(() => {
295
402
  alignItems: 'center',
296
403
  gap: 3,
297
404
  backgroundColor: originMeta.bg,
298
- paddingHorizontal: 6,
405
+ paddingHorizontal: 5,
299
406
  paddingVertical: 2,
300
407
  borderRadius: 4,
301
408
  borderWidth: 1,
302
409
  borderColor: originMeta.border,
303
410
  }}>
304
- <Text style={{ fontSize: 9 }}>{originMeta.icon}</Text>
411
+ {originMeta.renderIcon(originMeta.text, 8.5)}
305
412
  <Text style={{
306
413
  fontFamily: AppFonts.interBold,
307
414
  fontSize: 8.5,
@@ -328,18 +435,29 @@ const ReduxDetail = React.memo(() => {
328
435
  flexDirection: 'row',
329
436
  alignItems: 'center',
330
437
  justifyContent: 'space-between',
331
- marginTop: 5,
438
+ marginTop: 6,
332
439
  marginBottom: 2,
333
- paddingHorizontal: 7,
334
- paddingVertical: 3,
440
+ paddingHorizontal: 8,
441
+ paddingVertical: 4,
335
442
  backgroundColor: AppColors.grayBackground,
336
- borderRadius: 5,
443
+ borderRadius: 6,
337
444
  borderWidth: 1,
338
445
  borderColor: AppColors.dividerColor,
339
446
  }}>
340
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1, minWidth: 0 }}>
341
- <Text style={{ fontFamily: AppFonts.interMedium, fontSize: 9.5, color: AppColors.grayTextWeak }}>
342
- {t('redux.triggeredFrom')}
447
+ <View style={{
448
+ flexDirection: 'row',
449
+ alignItems: 'center',
450
+ gap: 5,
451
+ flex: 1,
452
+ minWidth: 0,
453
+ }}>
454
+ <DocIcon color={AppColors.sky600} size={10}/>
455
+ <Text style={{
456
+ fontFamily: AppFonts.interMedium,
457
+ fontSize: 9.5,
458
+ color: AppColors.grayTextWeak,
459
+ }}>
460
+ {t('redux.triggeredFrom', 'Triggered From:')}
343
461
  </Text>
344
462
  <Text style={{
345
463
  fontFamily: AppFonts.interBold,
@@ -356,12 +474,16 @@ const ReduxDetail = React.memo(() => {
356
474
  gap: 3,
357
475
  backgroundColor: AppColors.sky100,
358
476
  paddingHorizontal: 5,
359
- paddingVertical: 1.5,
360
- borderRadius: 3,
477
+ paddingVertical: 2,
478
+ borderRadius: 4,
361
479
  }}>
362
480
  <ExternalLinkIcon color={AppColors.sky600} size={9}/>
363
- <Text style={{ fontFamily: AppFonts.interBold, fontSize: 8.5, color: AppColors.sky600 }}>
364
- {t('redux.openInEditor')}
481
+ <Text style={{
482
+ fontFamily: AppFonts.interBold,
483
+ fontSize: 8.5,
484
+ color: AppColors.sky600,
485
+ }}>
486
+ {t('redux.openInEditor', 'Open in Editor')}
365
487
  </Text>
366
488
  </Pressable>
367
489
  </View>)}
@@ -369,9 +491,9 @@ const ReduxDetail = React.memo(() => {
369
491
  {/* Expanded Payload, Diff Viewer & Call Stack */}
370
492
  {isExpanded ? (<View style={reduxDetailStyles.expandedContent}>
371
493
  {/* Call Stack Trace Box */}
372
- {action.stack ? (<View style={{ marginBottom: 10 }}>
494
+ {action.stack ? (<View style={{ marginBottom: 12 }}>
373
495
  <Text style={reduxDetailStyles.expandedSectionTitle}>
374
- {t('redux.callStack')}:
496
+ {t('redux.callStack', 'Call Stack Trace')}:
375
497
  </Text>
376
498
  <View style={{
377
499
  backgroundColor: AppColors.grayBackground,
@@ -384,37 +506,48 @@ const ReduxDetail = React.memo(() => {
384
506
  {action.stack
385
507
  .split('\n')
386
508
  .map(l => l.trim())
387
- .filter(l => l.length > 0 && !l.includes('reduxLogger') && !l.includes('inspectorReduxMiddleware'))
509
+ .filter(l => l.length > 0 &&
510
+ !l.includes('reduxLogger') &&
511
+ !l.includes('inspectorReduxMiddleware'))
388
512
  .slice(0, 8)
389
513
  .map((frameLine, fIdx) => {
390
514
  const parsed = parseStackLine(frameLine, fIdx === 0);
391
515
  const isApp = parsed.frameType === 'app';
392
516
  return (<Pressable key={fIdx} onPress={() => {
393
517
  if (parsed.fileName !== 'Unknown') {
394
- openInVSCode(parsed.fullPath || parsed.fileName, parsed.lineNumber, parsed.columnNumber);
518
+ openInVSCode(parsed.fullPath ||
519
+ parsed.fileName, parsed.lineNumber, parsed.columnNumber);
395
520
  }
396
521
  }} style={{
397
522
  flexDirection: 'row',
398
523
  alignItems: 'center',
399
524
  justifyContent: 'space-between',
400
- paddingVertical: 2.5,
525
+ paddingVertical: 3,
401
526
  borderBottomWidth: fIdx < 7 ? 0.5 : 0,
402
527
  borderBottomColor: AppColors.dividerColor,
403
528
  }}>
404
529
  <View style={{ flex: 1, minWidth: 0 }}>
405
530
  <Text style={{
406
- fontFamily: isApp ? AppFonts.interBold : AppFonts.interRegular,
531
+ fontFamily: isApp
532
+ ? AppFonts.interBold
533
+ : AppFonts.interRegular,
407
534
  fontSize: 9.5,
408
- color: isApp ? AppColors.primaryBlack : AppColors.grayTextWeak,
535
+ color: isApp
536
+ ? AppColors.primaryBlack
537
+ : AppColors.grayTextWeak,
409
538
  }} numberOfLines={1}>
410
539
  {parsed.functionName || 'anonymous'}
411
540
  </Text>
412
541
  <Text style={{
413
542
  fontFamily: AppFonts.interRegular,
414
543
  fontSize: 8.5,
415
- color: isApp ? AppColors.sky600 : AppColors.grayTextWeak,
544
+ color: isApp
545
+ ? AppColors.sky600
546
+ : AppColors.grayTextWeak,
416
547
  }} numberOfLines={1}>
417
- {parsed.fileName}:{parsed.lineNumber || 1}:{parsed.columnNumber || 1}
548
+ {parsed.fileName}:
549
+ {parsed.lineNumber || 1}:
550
+ {parsed.columnNumber || 1}
418
551
  </Text>
419
552
  </View>
420
553
  {isApp && (<ExternalLinkIcon color={AppColors.sky600} size={10}/>)}
@@ -423,20 +556,20 @@ const ReduxDetail = React.memo(() => {
423
556
  </View>
424
557
  </View>) : null}
425
558
 
426
- {action.payload !== undefined && (<View style={{ marginBottom: 10 }}>
559
+ {action.payload !== undefined && (<View style={{ marginBottom: 12 }}>
427
560
  <Text style={reduxDetailStyles.expandedSectionTitle}>
428
- {t('redux.actionPayload')}
561
+ {t('redux.actionPayload', 'Action Payload:')}
429
562
  </Text>
430
- <JsonViewer data={action.payload} search={detailSearch} forceOpen={true}/>
563
+ <JsonViewer data={action.payload} search={detailSearch} forceOpen={true} wrap={true}/>
431
564
  </View>)}
432
565
 
433
566
  <Text style={reduxDetailStyles.expandedSectionTitle}>
434
- {t('redux.stateChangesDiff')}
567
+ {t('redux.stateChangesDiff', 'State Changes (Diff):')}
435
568
  </Text>
436
569
  <DiffViewer oldData={action.prevState?.[selectedReduxSlice] || {}} newData={action.nextState?.[selectedReduxSlice] || {}} forceOpen={true}/>
437
570
  </View>) : (<View style={reduxDetailStyles.collapsedPrompt}>
438
571
  <Text style={reduxDetailStyles.collapsedPromptText}>
439
- {t('redux.tapToInspectAction')}
572
+ {t('redux.tapToInspectAction', 'Tap to inspect action payload & diff changes')}
440
573
  </Text>
441
574
  <ForwardChevronIcon color={AppColors.grayTextWeak} size={12}/>
442
575
  </View>)}
@@ -451,7 +584,9 @@ const ReduxDetail = React.memo(() => {
451
584
  { label: 'In-Memory State Size', value: getSize(sliceData) },
452
585
  {
453
586
  label: 'Persistence Status',
454
- value: isPersisted ? `Persisted (${getSize(persistedData)})` : 'Not Persisted',
587
+ value: isPersisted
588
+ ? `Persisted (${getSize(persistedData)})`
589
+ : 'Not Persisted (In-Memory)',
455
590
  },
456
591
  {
457
592
  label: 'Dispatched Actions History',
@@ -482,12 +617,330 @@ const ReduxDetail = React.memo(() => {
482
617
  </Text>
483
618
  </TouchableOpacity>)}
484
619
  </View>
485
- </ScrollView>) : (<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 12, paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
486
- <JsonViewer data={sliceTab === 'live' ? sliceData : persistedData ?? 'No persisted state stored.'} search={detailSearch} forceOpen={detailSearch.length > 0 ? true : undefined} mode={viewMode} onModeChange={setViewMode}/>
487
- </ScrollView>)}
620
+ </ScrollView>) : sliceTab === 'persisted' ? (<View style={{ flex: 1, padding: 10 }}>
621
+ {isPersisted ? (<View style={{ flex: 1 }}>
622
+ <View style={reduxDetailStyles.persistedInfoCard}>
623
+ <View style={{
624
+ flexDirection: 'row',
625
+ alignItems: 'center',
626
+ justifyContent: 'space-between',
627
+ }}>
628
+ <View style={{
629
+ flexDirection: 'row',
630
+ alignItems: 'center',
631
+ gap: 6,
632
+ }}>
633
+ <StorageIcon color={AppColors.emerald700} size={14}/>
634
+ <Text style={reduxDetailStyles.persistedInfoTitle}>
635
+ Stored in persistent storage ({getSize(persistedData)})
636
+ </Text>
637
+ </View>
638
+ <TouchableOpacity style={reduxDetailStyles.clearPersistMiniBtn} onPress={handleClearPersistence}>
639
+ <TrashIcon color={AppColors.errorColor} size={12}/>
640
+ <Text style={reduxDetailStyles.clearPersistMiniText}>
641
+ Clear
642
+ </Text>
643
+ </TouchableOpacity>
644
+ </View>
645
+ </View>
646
+ <View style={{ flex: 1, marginTop: 8 }}>
647
+ <JsonViewer data={persistedData} search={detailSearch} forceOpen={detailSearch.length > 0 ? true : undefined} mode={viewMode} onModeChange={setViewMode} fullHeight={true} wrap={true}/>
648
+ </View>
649
+ </View>) : (<View style={reduxDetailStyles.emptyBox}>
650
+ <StorageIcon color={AppColors.grayTextWeak} size={32}/>
651
+ <Text style={reduxDetailStyles.emptyBoxTitle}>
652
+ Not Persisted in Storage
653
+ </Text>
654
+ <Text style={reduxDetailStyles.emptyBoxText}>
655
+ This slice is not stored in AsyncStorage or redux-persist.
656
+ State exists purely in active JavaScript memory.
657
+ </Text>
658
+ </View>)}
659
+ </View>) : (
660
+ /* Live State JsonViewer: Direct flex: 1 with fullHeight to prevent nested scroll conflicts */
661
+ <View style={{ flex: 1, padding: 10 }}>
662
+ <JsonViewer data={sliceData} search={detailSearch} forceOpen={detailSearch.length > 0 ? true : undefined} mode={viewMode} onModeChange={setViewMode} fullHeight={true} wrap={true}/>
663
+ </View>)}
664
+ </View>);
665
+ }
666
+ // ══════════════════════════════════════════════════════════════════════════
667
+ // ── 2. RENDER ACTION DETAIL VIEW (when an action is selected directly) ────
668
+ // ══════════════════════════════════════════════════════════════════════════
669
+ if (selectedReduxAction != null) {
670
+ const originMeta = getOriginBadge(selectedReduxAction.originType);
671
+ const callerBasename = selectedReduxAction.callerFile
672
+ ? selectedReduxAction.callerFile.split('/').pop()
673
+ : null;
674
+ const actionTabs = [
675
+ {
676
+ key: 'payload',
677
+ label: t('redux.payload', 'Payload'),
678
+ icon: (isActive) => (<CodeBracketsIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
679
+ },
680
+ {
681
+ key: 'diff',
682
+ label: t('redux.diff', 'State Diff'),
683
+ icon: (isActive) => (<TimelineIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
684
+ },
685
+ {
686
+ key: 'stack',
687
+ label: t('redux.callStack', 'Call Stack'),
688
+ icon: (isActive) => (<DocIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
689
+ },
690
+ {
691
+ key: 'raw',
692
+ label: 'Raw Action',
693
+ icon: (isActive) => (<LiveStateIcon color={isActive ? AppColors.white : AppColors.grayText} size={12}/>),
694
+ },
695
+ ];
696
+ return (<View style={{ flex: 1, backgroundColor: AppColors.grayBackground }}>
697
+ {/* Top Info Bar */}
698
+ <View style={reduxDetailStyles.infoBar}>
699
+ <View style={reduxDetailStyles.infoTopRow}>
700
+ <View style={{
701
+ flexDirection: 'row',
702
+ alignItems: 'center',
703
+ gap: 6,
704
+ flexWrap: 'wrap',
705
+ flex: 1,
706
+ minWidth: 0,
707
+ }}>
708
+ <View style={reduxDetailStyles.actionBadge}>
709
+ <Text style={reduxDetailStyles.actionBadgeText}>
710
+ #{selectedReduxAction.id}
711
+ </Text>
712
+ </View>
713
+ {/* Origin Badge */}
714
+ <View style={{
715
+ flexDirection: 'row',
716
+ alignItems: 'center',
717
+ gap: 3,
718
+ backgroundColor: originMeta.bg,
719
+ paddingHorizontal: 5,
720
+ paddingVertical: 2,
721
+ borderRadius: 4,
722
+ borderWidth: 1,
723
+ borderColor: originMeta.border,
724
+ }}>
725
+ {originMeta.renderIcon(originMeta.text, 8.5)}
726
+ <Text style={{
727
+ fontFamily: AppFonts.interBold,
728
+ fontSize: 8.5,
729
+ color: originMeta.text,
730
+ letterSpacing: 0.3,
731
+ }}>
732
+ {originMeta.label}
733
+ </Text>
734
+ </View>
735
+ <Text style={reduxDetailStyles.sliceNameText} numberOfLines={1}>
736
+ {selectedReduxAction.type}
737
+ </Text>
738
+ {selectedReduxAction.timestamp && (<View style={reduxDetailStyles.timePill}>
739
+ <ClockIcon color={AppColors.purple} size={10}/>
740
+ <Text style={reduxDetailStyles.timeText}>
741
+ {selectedReduxAction.timestamp}
742
+ </Text>
743
+ </View>)}
744
+ </View>
745
+
746
+ {/* Copy Button */}
747
+ <CopyButton value={() => selectedReduxAction} label="Copy Action"/>
748
+ </View>
749
+
750
+ {/* Sub Stats Row */}
751
+ <View style={reduxDetailStyles.subStatsRow}>
752
+ {selectedReduxAction.affectedSlices &&
753
+ selectedReduxAction.affectedSlices.length > 0 && (<View style={[
754
+ reduxDetailStyles.statPill,
755
+ {
756
+ backgroundColor: AppColors.indigo50,
757
+ borderColor: AppColors.indigo400,
758
+ },
759
+ ]}>
760
+ <LayersIcon color={AppColors.indigo600Alt} size={11}/>
761
+ <Text style={[
762
+ reduxDetailStyles.statLabel,
763
+ { color: AppColors.indigo600Alt },
764
+ ]}>
765
+ Slices:
766
+ </Text>
767
+ <Text style={[
768
+ reduxDetailStyles.statValue,
769
+ { color: AppColors.indigo600Alt },
770
+ ]}>
771
+ {selectedReduxAction.affectedSlices.join(', ')}
772
+ </Text>
773
+ </View>)}
774
+ <View style={[
775
+ reduxDetailStyles.statPill,
776
+ {
777
+ backgroundColor: AppColors.sky100,
778
+ borderColor: AppColors.sky400,
779
+ },
780
+ ]}>
781
+ <Text style={[
782
+ reduxDetailStyles.statLabel,
783
+ { color: AppColors.sky600 },
784
+ ]}>
785
+ Payload Size:
786
+ </Text>
787
+ <Text style={[
788
+ reduxDetailStyles.statValue,
789
+ { color: AppColors.sky600 },
790
+ ]}>
791
+ {getSize(selectedReduxAction.payload)}
792
+ </Text>
793
+ </View>
794
+ </View>
795
+
796
+ {/* Triggered from caller preview row */}
797
+ {selectedReduxAction.callerFile && (<View style={{
798
+ flexDirection: 'row',
799
+ alignItems: 'center',
800
+ justifyContent: 'space-between',
801
+ marginTop: 8,
802
+ paddingHorizontal: 8,
803
+ paddingVertical: 4,
804
+ backgroundColor: AppColors.grayBackground,
805
+ borderRadius: 6,
806
+ borderWidth: 1,
807
+ borderColor: AppColors.dividerColor,
808
+ }}>
809
+ <View style={{
810
+ flexDirection: 'row',
811
+ alignItems: 'center',
812
+ gap: 5,
813
+ flex: 1,
814
+ minWidth: 0,
815
+ }}>
816
+ <DocIcon color={AppColors.sky600} size={10}/>
817
+ <Text style={{
818
+ fontFamily: AppFonts.interMedium,
819
+ fontSize: 9.5,
820
+ color: AppColors.grayTextWeak,
821
+ }}>
822
+ Triggered from:
823
+ </Text>
824
+ <Text style={{
825
+ fontFamily: AppFonts.interBold,
826
+ fontSize: 9.5,
827
+ color: AppColors.sky600,
828
+ }} numberOfLines={1}>
829
+ {callerBasename}:{selectedReduxAction.callerLine || 1}
830
+ </Text>
831
+ </View>
832
+
833
+ <Pressable onPress={() => openInVSCode(selectedReduxAction.callerFile, selectedReduxAction.callerLine, selectedReduxAction.callerCol)} hitSlop={8} style={{
834
+ flexDirection: 'row',
835
+ alignItems: 'center',
836
+ gap: 3,
837
+ backgroundColor: AppColors.sky100,
838
+ paddingHorizontal: 5,
839
+ paddingVertical: 2,
840
+ borderRadius: 4,
841
+ }}>
842
+ <ExternalLinkIcon color={AppColors.sky600} size={9}/>
843
+ <Text style={{
844
+ fontFamily: AppFonts.interBold,
845
+ fontSize: 8.5,
846
+ color: AppColors.sky600,
847
+ }}>
848
+ Open in Editor
849
+ </Text>
850
+ </Pressable>
851
+ </View>)}
852
+
853
+ {/* Sub-Tabs */}
854
+ <SegmentedTabs tabs={actionTabs} activeKey={actionTab} onChange={key => setActionTab(key)} style={{ marginTop: 10 }}/>
855
+ </View>
856
+
857
+ {/* Content Body */}
858
+ {actionTab === 'payload' ? (<View style={{ flex: 1, padding: 10 }}>
859
+ {selectedReduxAction.payload !== undefined ? (<JsonViewer data={selectedReduxAction.payload} search={detailSearch} forceOpen={true} fullHeight={true} wrap={true}/>) : (<View style={reduxDetailStyles.emptyBox}>
860
+ <Text style={reduxDetailStyles.emptyBoxText}>
861
+ This action has no payload dispatched.
862
+ </Text>
863
+ </View>)}
864
+ </View>) : actionTab === 'diff' ? (<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 12, paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
865
+ <DiffViewer oldData={selectedReduxAction.prevState || {}} newData={selectedReduxAction.nextState || {}} forceOpen={true}/>
866
+ </ScrollView>) : actionTab === 'stack' ? (<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 12, paddingBottom: 40 }} keyboardShouldPersistTaps="handled">
867
+ {selectedReduxAction.stack ? (<View style={{
868
+ backgroundColor: AppColors.white,
869
+ borderRadius: 10,
870
+ padding: 10,
871
+ borderWidth: 1,
872
+ borderColor: AppColors.dividerColor,
873
+ }}>
874
+ <Text style={reduxDetailStyles.expandedSectionTitle}>
875
+ Dispatched from call stack:
876
+ </Text>
877
+ {selectedReduxAction.stack
878
+ .split('\n')
879
+ .map(l => l.trim())
880
+ .filter(l => l.length > 0 &&
881
+ !l.includes('reduxLogger') &&
882
+ !l.includes('inspectorReduxMiddleware'))
883
+ .map((frameLine, fIdx) => {
884
+ const parsed = parseStackLine(frameLine, fIdx === 0);
885
+ const isApp = parsed.frameType === 'app';
886
+ return (<Pressable key={fIdx} onPress={() => {
887
+ if (parsed.fileName !== 'Unknown') {
888
+ openInVSCode(parsed.fullPath || parsed.fileName, parsed.lineNumber, parsed.columnNumber);
889
+ }
890
+ }} style={{
891
+ flexDirection: 'row',
892
+ alignItems: 'center',
893
+ justifyContent: 'space-between',
894
+ paddingVertical: 5,
895
+ borderBottomWidth: 0.5,
896
+ borderBottomColor: AppColors.dividerColor,
897
+ }}>
898
+ <View style={{ flex: 1, minWidth: 0 }}>
899
+ <Text style={{
900
+ fontFamily: isApp
901
+ ? AppFonts.interBold
902
+ : AppFonts.interRegular,
903
+ fontSize: 10.5,
904
+ color: isApp
905
+ ? AppColors.primaryBlack
906
+ : AppColors.grayTextWeak,
907
+ }} numberOfLines={1}>
908
+ {parsed.functionName || 'anonymous'}
909
+ </Text>
910
+ <Text style={{
911
+ fontFamily: AppFonts.interRegular,
912
+ fontSize: 9,
913
+ color: isApp
914
+ ? AppColors.sky600
915
+ : AppColors.grayTextWeak,
916
+ }} numberOfLines={1}>
917
+ {parsed.fileName}:{parsed.lineNumber || 1}:
918
+ {parsed.columnNumber || 1}
919
+ </Text>
920
+ </View>
921
+ {isApp && (<ExternalLinkIcon color={AppColors.sky600} size={11}/>)}
922
+ </Pressable>);
923
+ })}
924
+ </View>) : (<View style={reduxDetailStyles.emptyBox}>
925
+ <Text style={reduxDetailStyles.emptyBoxText}>
926
+ No call stack trace captured for this action.
927
+ </Text>
928
+ </View>)}
929
+ </ScrollView>) : (<View style={{ flex: 1, padding: 10 }}>
930
+ <JsonViewer data={selectedReduxAction} forceOpen={true} fullHeight={true} wrap={true}/>
931
+ </View>)}
488
932
  </View>);
489
933
  }
490
- return null;
934
+ return (<View style={reduxDetailStyles.emptyBox}>
935
+ <TerminalIcon color={AppColors.purple} size={32}/>
936
+ <Text style={reduxDetailStyles.emptyBoxTitle}>
937
+ No Redux Slice Selected
938
+ </Text>
939
+ <Text style={reduxDetailStyles.emptyBoxText}>
940
+ Select a slice from the Redux tab to inspect its state and dispatched
941
+ actions.
942
+ </Text>
943
+ </View>);
491
944
  });
492
945
  const reduxDetailStyles = StyleSheet.create({
493
946
  infoBar: {
@@ -503,20 +956,21 @@ const reduxDetailStyles = StyleSheet.create({
503
956
  marginBottom: 8,
504
957
  },
505
958
  methodBadge: {
506
- paddingHorizontal: 8,
507
- paddingVertical: 3,
508
- borderRadius: 6,
959
+ paddingHorizontal: 7,
960
+ paddingVertical: 2.5,
961
+ borderRadius: 5,
509
962
  },
510
963
  methodBadgeText: {
511
964
  fontFamily: AppFonts.interBold,
512
- fontSize: 10,
965
+ fontSize: 9.5,
513
966
  color: AppColors.white,
514
- letterSpacing: 0.5,
967
+ letterSpacing: 0.4,
515
968
  },
516
969
  sliceNameText: {
517
970
  fontFamily: AppFonts.interBold,
518
971
  fontSize: 14,
519
972
  color: AppColors.primaryBlack,
973
+ flexShrink: 1,
520
974
  },
521
975
  persistedChip: {
522
976
  flexDirection: 'row',
@@ -601,6 +1055,11 @@ const reduxDetailStyles = StyleSheet.create({
601
1055
  marginBottom: 8,
602
1056
  borderWidth: 1,
603
1057
  borderColor: AppColors.dividerColor,
1058
+ shadowColor: AppColors.primaryBlack,
1059
+ shadowOffset: { width: 0, height: 1 },
1060
+ shadowOpacity: 0.03,
1061
+ shadowRadius: 3,
1062
+ elevation: 1,
604
1063
  },
605
1064
  timelineHeader: {
606
1065
  flexDirection: 'row',
@@ -664,7 +1123,7 @@ const reduxDetailStyles = StyleSheet.create({
664
1123
  fontFamily: AppFonts.interBold,
665
1124
  fontSize: 11,
666
1125
  color: AppColors.grayTextStrong,
667
- marginBottom: 4,
1126
+ marginBottom: 6,
668
1127
  },
669
1128
  metadataCard: {
670
1129
  backgroundColor: AppColors.white,
@@ -672,6 +1131,11 @@ const reduxDetailStyles = StyleSheet.create({
672
1131
  padding: 12,
673
1132
  borderWidth: 1,
674
1133
  borderColor: AppColors.dividerColor,
1134
+ shadowColor: AppColors.primaryBlack,
1135
+ shadowOffset: { width: 0, height: 1 },
1136
+ shadowOpacity: 0.03,
1137
+ shadowRadius: 3,
1138
+ elevation: 1,
675
1139
  },
676
1140
  metaRow: {
677
1141
  flexDirection: 'row',
@@ -693,6 +1157,32 @@ const reduxDetailStyles = StyleSheet.create({
693
1157
  maxWidth: '60%',
694
1158
  textAlign: 'right',
695
1159
  },
1160
+ persistedInfoCard: {
1161
+ backgroundColor: '#ECFDF5',
1162
+ borderRadius: 8,
1163
+ borderWidth: 1,
1164
+ borderColor: '#A7F3D0',
1165
+ padding: 10,
1166
+ },
1167
+ persistedInfoTitle: {
1168
+ fontFamily: AppFonts.interBold,
1169
+ fontSize: 11,
1170
+ color: '#047857',
1171
+ },
1172
+ clearPersistMiniBtn: {
1173
+ flexDirection: 'row',
1174
+ alignItems: 'center',
1175
+ gap: 3,
1176
+ backgroundColor: '#FEE2E2',
1177
+ paddingHorizontal: 6,
1178
+ paddingVertical: 2.5,
1179
+ borderRadius: 4,
1180
+ },
1181
+ clearPersistMiniText: {
1182
+ fontFamily: AppFonts.interBold,
1183
+ fontSize: 9.5,
1184
+ color: AppColors.errorColor,
1185
+ },
696
1186
  clearPersistBtn: {
697
1187
  flexDirection: 'row',
698
1188
  alignItems: 'center',
@@ -718,11 +1208,20 @@ const reduxDetailStyles = StyleSheet.create({
718
1208
  justifyContent: 'center',
719
1209
  borderWidth: 1,
720
1210
  borderColor: AppColors.dividerColor,
1211
+ margin: 12,
1212
+ gap: 8,
1213
+ },
1214
+ emptyBoxTitle: {
1215
+ fontFamily: AppFonts.interBold,
1216
+ fontSize: 14,
1217
+ color: AppColors.primaryBlack,
721
1218
  },
722
1219
  emptyBoxText: {
723
1220
  fontFamily: AppFonts.interMedium,
724
- fontSize: 12.5,
1221
+ fontSize: 12,
725
1222
  color: AppColors.grayTextWeak,
1223
+ textAlign: 'center',
1224
+ lineHeight: 18,
726
1225
  },
727
1226
  highlight: {
728
1227
  backgroundColor: '#FEF08A',