react-native-inapp-inspector 2.2.2 → 2.2.3

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.
Files changed (97) hide show
  1. package/dist/commonjs/constants/version.d.ts +1 -1
  2. package/dist/commonjs/constants/version.js +1 -1
  3. package/dist/esm/constants/version.d.ts +1 -1
  4. package/dist/esm/constants/version.js +1 -1
  5. package/package.json +2 -1
  6. package/src/analytics.ts +35 -0
  7. package/src/bundle.ts +25 -0
  8. package/src/components/AnalyticsDetail.tsx +865 -0
  9. package/src/components/AnalyticsEventCard.tsx +441 -0
  10. package/src/components/AnalyticsGraph.tsx +583 -0
  11. package/src/components/AnimatedEntrance.tsx +64 -0
  12. package/src/components/AppHeaderLogo.tsx +109 -0
  13. package/src/components/BrandCircleIcon.tsx +144 -0
  14. package/src/components/BrandSquareIcon.tsx +144 -0
  15. package/src/components/CodeSnippet.tsx +725 -0
  16. package/src/components/ConsoleLogCard.tsx +628 -0
  17. package/src/components/CopyButton.tsx +87 -0
  18. package/src/components/DiffViewer.tsx +82 -0
  19. package/src/components/DomainHeader.tsx +237 -0
  20. package/src/components/EmptyState.tsx +73 -0
  21. package/src/components/EndOfListFooter.tsx +100 -0
  22. package/src/components/ErrorBoundary.tsx +688 -0
  23. package/src/components/HeadersSection.tsx +192 -0
  24. package/src/components/HighlightText.tsx +100 -0
  25. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  26. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  27. package/src/components/Inspector/BundleTab.tsx +4731 -0
  28. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  29. package/src/components/Inspector/CrashDetail.tsx +941 -0
  30. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  31. package/src/components/Inspector/CrashTab.tsx +871 -0
  32. package/src/components/Inspector/FabLauncher.tsx +80 -0
  33. package/src/components/Inspector/InspectorContext.tsx +28 -0
  34. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  35. package/src/components/Inspector/LogDetail.tsx +1427 -0
  36. package/src/components/Inspector/MainScreen.tsx +258 -0
  37. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  38. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  39. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  40. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  41. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  42. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  43. package/src/components/Inspector/ReduxTab.tsx +954 -0
  44. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  45. package/src/components/Inspector/TabBar.tsx +187 -0
  46. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  47. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  48. package/src/components/JsonViewer.tsx +486 -0
  49. package/src/components/LogCard.tsx +491 -0
  50. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  51. package/src/components/MetaAccordion.tsx +340 -0
  52. package/src/components/MiniBarChart.tsx +42 -0
  53. package/src/components/MiniLineChart.tsx +33 -0
  54. package/src/components/NetworkIcons.tsx +2118 -0
  55. package/src/components/SectionHeader.tsx +113 -0
  56. package/src/components/SegmentedTabs.tsx +83 -0
  57. package/src/components/Slider.tsx +300 -0
  58. package/src/components/SourcePageCard.tsx +148 -0
  59. package/src/components/Toast.tsx +131 -0
  60. package/src/components/TouchableScale.tsx +91 -0
  61. package/src/components/TreeNode.tsx +186 -0
  62. package/src/console.ts +24 -0
  63. package/src/constants/index.ts +38 -0
  64. package/src/constants/version.ts +3 -0
  65. package/src/crash.ts +32 -0
  66. package/src/customHooks/analyticsLogger.ts +336 -0
  67. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  68. package/src/customHooks/consoleLogger.ts +497 -0
  69. package/src/customHooks/crashHandler.ts +944 -0
  70. package/src/customHooks/logFilters.ts +32 -0
  71. package/src/customHooks/networkLogger.ts +419 -0
  72. package/src/customHooks/performanceTracker.ts +1014 -0
  73. package/src/customHooks/reduxLogger.ts +406 -0
  74. package/src/customHooks/useAccordion.tsx +60 -0
  75. package/src/decorators/index.ts +184 -0
  76. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  77. package/src/helpers/index.ts +860 -0
  78. package/src/helpers/memoryManager.ts +138 -0
  79. package/src/helpers/searchQueryParser.ts +283 -0
  80. package/src/helpers/settingsStore.ts +171 -0
  81. package/src/helpers/telemetry.ts +505 -0
  82. package/src/helpers/toast.ts +17 -0
  83. package/src/i18n/index.ts +69 -0
  84. package/src/i18n/locales/en.json +1052 -0
  85. package/src/index.tsx +2336 -0
  86. package/src/native/NativeInspector.ts +397 -0
  87. package/src/native/NativeNetworkInspector.ts +24 -0
  88. package/src/network.ts +22 -0
  89. package/src/performance.ts +38 -0
  90. package/src/redux.ts +23 -0
  91. package/src/styles/AppColors.ts +408 -0
  92. package/src/styles/AppFonts.ts +8 -0
  93. package/src/styles/common.ts +209 -0
  94. package/src/styles/index.ts +1570 -0
  95. package/src/types/enums.ts +194 -0
  96. package/src/types/index.ts +34 -0
  97. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,1651 @@
1
+ import React, {useState, useMemo, useEffect, useCallback} from 'react';
2
+ import {
3
+ Alert,
4
+ Platform,
5
+ Pressable,
6
+ ScrollView,
7
+ StyleSheet,
8
+ Text,
9
+ TextInput,
10
+ ToastAndroid,
11
+ TouchableOpacity,
12
+ View,
13
+ } from 'react-native';
14
+ import {useTranslation} from '../../i18n';
15
+ import {useInspector} from './InspectorContext';
16
+ import SegmentedTabs from '../SegmentedTabs';
17
+ import JsonViewer from '../JsonViewer';
18
+ import DiffViewer from '../DiffViewer';
19
+ import CopyButton from '../CopyButton';
20
+ import TouchableScale from '../TouchableScale';
21
+ import HighlightText from '../HighlightText';
22
+ import AnimatedEntrance from '../AnimatedEntrance';
23
+ import {getActionHistory} from '../../customHooks/reduxLogger';
24
+ import {getSize, openInVSCode, parseStackLine} from '../../helpers';
25
+ import {getCustomStorage} from '../../helpers/settingsStore';
26
+ import {AppColors} from '../../styles/AppColors';
27
+ import {AppFonts} from '../../styles/AppFonts';
28
+ import {
29
+ TerminalIcon,
30
+ SearchIcon,
31
+ ClearIcon,
32
+ ClockIcon,
33
+ LayersIcon,
34
+ TrashIcon,
35
+ ForwardChevronIcon,
36
+ LiveStateIcon,
37
+ TimelineIcon,
38
+ StorageIcon,
39
+ MetadataIcon,
40
+ BoltIcon,
41
+ AtomIcon,
42
+ ScreenIcon,
43
+ ListenerIcon,
44
+ DocIcon,
45
+ LoadingSpinnerIcon,
46
+ CircleAlertIcon,
47
+ ExternalLinkIcon,
48
+ SizeIcon,
49
+ CodeBracketsIcon,
50
+ } from '../NetworkIcons';
51
+
52
+ type SliceDetailSubTab = 'live' | 'timeline' | 'persisted' | 'metadata';
53
+ type ActionDetailSubTab = 'payload' | 'diff' | 'stack' | 'raw';
54
+
55
+ const getOriginBadge = (originType?: string) => {
56
+ switch (originType) {
57
+ case 'saga':
58
+ return {
59
+ label: 'SAGA',
60
+ renderIcon: (color: string, size = 9) => (
61
+ <BoltIcon color={color} size={size} />
62
+ ),
63
+ bg: AppColors.purple100,
64
+ text: AppColors.brandPurple,
65
+ border: AppColors.purple200,
66
+ };
67
+ case 'thunk':
68
+ return {
69
+ label: 'THUNK',
70
+ renderIcon: (color: string, size = 9) => (
71
+ <AtomIcon color={color} size={size} />
72
+ ),
73
+ bg: AppColors.amber100,
74
+ text: AppColors.amber800Warm,
75
+ border: AppColors.amber200,
76
+ };
77
+ case 'ui':
78
+ return {
79
+ label: 'UI',
80
+ renderIcon: (color: string, size = 9) => (
81
+ <ScreenIcon color={color} size={size} />
82
+ ),
83
+ bg: AppColors.sky100,
84
+ text: AppColors.sky600,
85
+ border: AppColors.sky400,
86
+ };
87
+ case 'listener':
88
+ return {
89
+ label: 'LISTENER',
90
+ renderIcon: (color: string, size = 9) => (
91
+ <ListenerIcon color={color} size={size} />
92
+ ),
93
+ bg: AppColors.teal100,
94
+ text: AppColors.teal700,
95
+ border: AppColors.teal400,
96
+ };
97
+ default:
98
+ return {
99
+ label: 'DIRECT',
100
+ renderIcon: (color: string, size = 9) => (
101
+ <BoltIcon color={color} size={size} />
102
+ ),
103
+ bg: AppColors.slate100,
104
+ text: AppColors.slate700,
105
+ border: AppColors.slate200,
106
+ };
107
+ }
108
+ };
109
+
110
+ const ReduxDetail = React.memo(() => {
111
+ const {t} = useTranslation();
112
+ const {
113
+ reduxState,
114
+ reduxLastActionMap,
115
+ selectedReduxSlice,
116
+ selectedReduxAction,
117
+ setSelectedReduxSlice,
118
+ } = useInspector();
119
+
120
+ // Search filter within details
121
+ const [detailSearch, setDetailSearch] = useState('');
122
+
123
+ // Sub-tabs for Slice & Action
124
+ const [sliceTab, setSliceTab] = useState<SliceDetailSubTab>('live');
125
+ const [actionTab, setActionTab] = useState<ActionDetailSubTab>('payload');
126
+ const [viewMode, setViewMode] = useState<'pretty' | 'raw' | 'table'>(
127
+ 'pretty',
128
+ );
129
+
130
+ // Selected Action inside Timeline
131
+ const [expandedActionId, setExpandedActionId] = useState<number | null>(null);
132
+
133
+ // Persistence data for active slice
134
+ const [isPersisted, setIsPersisted] = useState(false);
135
+ const [persistedData, setPersistedData] = useState<any>(null);
136
+
137
+ // Fetch Action History
138
+ const allActionHistory = useMemo(() => {
139
+ return getActionHistory();
140
+ }, [reduxLastActionMap]);
141
+
142
+ // Slices-filtered actions
143
+ const sliceActions = useMemo(() => {
144
+ if (!selectedReduxSlice) return allActionHistory;
145
+ return allActionHistory.filter(action => {
146
+ if (!action.affectedSlices || action.affectedSlices.length === 0)
147
+ return true;
148
+ return action.affectedSlices.includes(selectedReduxSlice);
149
+ });
150
+ }, [allActionHistory, selectedReduxSlice]);
151
+
152
+ // Filtered slice actions by search query in timeline tab
153
+ const filteredTimelineActions = useMemo(() => {
154
+ if (!detailSearch.trim()) return sliceActions;
155
+ const q = detailSearch.toLowerCase();
156
+ return sliceActions.filter(action => {
157
+ return (
158
+ action.type.toLowerCase().includes(q) ||
159
+ (action.callerFile && action.callerFile.toLowerCase().includes(q)) ||
160
+ (action.payload &&
161
+ JSON.stringify(action.payload).toLowerCase().includes(q))
162
+ );
163
+ });
164
+ }, [sliceActions, detailSearch]);
165
+
166
+ useEffect(() => {
167
+ if (!selectedReduxSlice) return;
168
+
169
+ async function checkPersistence() {
170
+ try {
171
+ const storage = getCustomStorage();
172
+ if (!storage) {
173
+ setIsPersisted(false);
174
+ setPersistedData(null);
175
+ return;
176
+ }
177
+
178
+ let raw = await storage.getItem(`persist:${selectedReduxSlice}`);
179
+ if (raw) {
180
+ try {
181
+ setPersistedData(JSON.parse(raw));
182
+ } catch {
183
+ setPersistedData(raw);
184
+ }
185
+ setIsPersisted(true);
186
+ return;
187
+ }
188
+
189
+ let rootRaw = await storage.getItem('persist:root');
190
+ if (rootRaw) {
191
+ const parsedRoot = JSON.parse(rootRaw);
192
+ if (parsedRoot && parsedRoot[selectedReduxSlice] !== undefined) {
193
+ const nested = parsedRoot[selectedReduxSlice];
194
+ try {
195
+ setPersistedData(
196
+ typeof nested === 'string' ? JSON.parse(nested) : nested,
197
+ );
198
+ } catch {
199
+ setPersistedData(nested);
200
+ }
201
+ setIsPersisted(true);
202
+ return;
203
+ }
204
+ }
205
+ } catch (e) {
206
+ // silent catch
207
+ }
208
+ setIsPersisted(false);
209
+ setPersistedData(null);
210
+ }
211
+
212
+ checkPersistence();
213
+ }, [selectedReduxSlice]);
214
+
215
+ const handleClearPersistence = () => {
216
+ if (!selectedReduxSlice) return;
217
+ Alert.alert(
218
+ 'Clear Persisted State',
219
+ `Are you sure you want to delete persisted storage for slice "${selectedReduxSlice}"?`,
220
+ [
221
+ {text: 'Cancel', style: 'cancel'},
222
+ {
223
+ text: 'Delete',
224
+ style: 'destructive',
225
+ onPress: async () => {
226
+ try {
227
+ const storage = getCustomStorage();
228
+ if (!storage) return;
229
+
230
+ if (typeof (storage as any).removeItem === 'function') {
231
+ await (storage as any).removeItem(
232
+ `persist:${selectedReduxSlice}`,
233
+ );
234
+ } else {
235
+ await storage.setItem(`persist:${selectedReduxSlice}`, '');
236
+ }
237
+
238
+ setIsPersisted(false);
239
+ setPersistedData(null);
240
+ setSliceTab('live');
241
+
242
+ if (Platform.OS === 'android') {
243
+ ToastAndroid.show(
244
+ 'Persisted state cleared',
245
+ ToastAndroid.SHORT,
246
+ );
247
+ } else {
248
+ Alert.alert('Cleared', 'Persisted state has been cleared.');
249
+ }
250
+ } catch (err) {
251
+ Alert.alert('Error', 'Failed to clear persisted state.');
252
+ }
253
+ },
254
+ },
255
+ ],
256
+ );
257
+ };
258
+
259
+ // ══════════════════════════════════════════════════════════════════════════
260
+ // ── 1. RENDER SLICE DETAIL VIEW ───────────────────────────────────────────
261
+ // ══════════════════════════════════════════════════════════════════════════
262
+ if (selectedReduxSlice != null) {
263
+ const sliceData = reduxState?.[selectedReduxSlice];
264
+ const fieldsCount =
265
+ sliceData && typeof sliceData === 'object'
266
+ ? Object.keys(sliceData).length
267
+ : typeof sliceData !== 'undefined'
268
+ ? 1
269
+ : 0;
270
+ const lastAction = reduxLastActionMap[selectedReduxSlice];
271
+
272
+ const sliceTabs = [
273
+ {
274
+ key: 'live',
275
+ label: t('redux.liveState', 'Live State'),
276
+ icon: (isActive: boolean) => (
277
+ <LiveStateIcon
278
+ color={isActive ? AppColors.white : AppColors.grayText}
279
+ size={12}
280
+ />
281
+ ),
282
+ },
283
+ {
284
+ key: 'timeline',
285
+ label: `${t('redux.timeline', 'Timeline')} (${sliceActions.length})`,
286
+ icon: (isActive: boolean) => (
287
+ <TimelineIcon
288
+ color={isActive ? AppColors.white : AppColors.grayText}
289
+ size={12}
290
+ />
291
+ ),
292
+ },
293
+ {
294
+ key: 'persisted',
295
+ label: isPersisted
296
+ ? t('redux.persisted', 'Persisted')
297
+ : t('redux.storage', 'Storage'),
298
+ icon: (isActive: boolean) => (
299
+ <StorageIcon
300
+ color={isActive ? AppColors.white : AppColors.grayText}
301
+ size={12}
302
+ />
303
+ ),
304
+ },
305
+ {
306
+ key: 'metadata',
307
+ label: t('redux.metadata', 'Metadata'),
308
+ icon: (isActive: boolean) => (
309
+ <MetadataIcon
310
+ color={isActive ? AppColors.white : AppColors.grayText}
311
+ size={12}
312
+ />
313
+ ),
314
+ },
315
+ ];
316
+
317
+ return (
318
+ <View style={{flex: 1, backgroundColor: AppColors.grayBackground}}>
319
+ {/* Info Header Bar */}
320
+ <View style={reduxDetailStyles.infoBar}>
321
+ <View style={reduxDetailStyles.infoTopRow}>
322
+ <View
323
+ style={{
324
+ flexDirection: 'row',
325
+ alignItems: 'center',
326
+ gap: 6,
327
+ flexWrap: 'wrap',
328
+ flex: 1,
329
+ minWidth: 0,
330
+ }}>
331
+ <View
332
+ style={[
333
+ reduxDetailStyles.methodBadge,
334
+ {backgroundColor: AppColors.indigo600Alt},
335
+ ]}>
336
+ <Text style={reduxDetailStyles.methodBadgeText}>
337
+ {t('redux.slice', 'SLICE')}
338
+ </Text>
339
+ </View>
340
+ <Text style={reduxDetailStyles.sliceNameText} numberOfLines={1}>
341
+ {selectedReduxSlice}
342
+ </Text>
343
+ {isPersisted ? (
344
+ <View style={reduxDetailStyles.persistedChip}>
345
+ <StorageIcon color={AppColors.emerald700} size={10} />
346
+ <Text style={reduxDetailStyles.persistedChipText}>
347
+ {t('redux.persisted', 'Persisted').toUpperCase()}
348
+ </Text>
349
+ </View>
350
+ ) : (
351
+ <View style={reduxDetailStyles.inMemoryChip}>
352
+ <Text style={reduxDetailStyles.inMemoryChipText}>
353
+ {t('redux.inMemory', 'In-Memory').toUpperCase()}
354
+ </Text>
355
+ </View>
356
+ )}
357
+ {lastAction?.timestamp && (
358
+ <View style={reduxDetailStyles.timePill}>
359
+ <ClockIcon color={AppColors.violet600} size={10} />
360
+ <Text style={reduxDetailStyles.timeText}>
361
+ {lastAction.timestamp}
362
+ </Text>
363
+ </View>
364
+ )}
365
+ </View>
366
+
367
+ {/* Copy Button */}
368
+ <CopyButton
369
+ value={() => (sliceTab === 'live' ? sliceData : persistedData)}
370
+ label={t('redux.sliceJson', 'Slice JSON')}
371
+ />
372
+ </View>
373
+
374
+ {/* Sub Stats Row */}
375
+ <View style={reduxDetailStyles.subStatsRow}>
376
+ <View
377
+ style={[
378
+ reduxDetailStyles.statPill,
379
+ {
380
+ backgroundColor: AppColors.indigo50,
381
+ borderColor: AppColors.indigo400,
382
+ },
383
+ ]}>
384
+ <LayersIcon color={AppColors.indigo600Alt} size={11} />
385
+ <Text
386
+ style={[
387
+ reduxDetailStyles.statLabel,
388
+ {color: AppColors.indigo600Alt},
389
+ ]}>
390
+ {t('redux.keys', 'Keys')}:
391
+ </Text>
392
+ <Text
393
+ style={[
394
+ reduxDetailStyles.statValue,
395
+ {color: AppColors.indigo600Alt},
396
+ ]}>
397
+ {fieldsCount}
398
+ </Text>
399
+ </View>
400
+ <View
401
+ style={[
402
+ reduxDetailStyles.statPill,
403
+ {
404
+ backgroundColor: AppColors.sky100,
405
+ borderColor: AppColors.sky400,
406
+ },
407
+ ]}>
408
+ <Text
409
+ style={[
410
+ reduxDetailStyles.statLabel,
411
+ {color: AppColors.sky600},
412
+ ]}>
413
+ {t('redux.size', 'Size')}:
414
+ </Text>
415
+ <Text
416
+ style={[
417
+ reduxDetailStyles.statValue,
418
+ {color: AppColors.sky600},
419
+ ]}>
420
+ {getSize(sliceData)}
421
+ </Text>
422
+ </View>
423
+ <View
424
+ style={[
425
+ reduxDetailStyles.statPill,
426
+ {
427
+ backgroundColor: AppColors.purple100,
428
+ borderColor: AppColors.purple200,
429
+ },
430
+ ]}>
431
+ <TimelineIcon color={AppColors.violet600} size={11} />
432
+ <Text
433
+ style={[
434
+ reduxDetailStyles.statLabel,
435
+ {color: AppColors.brandPurple},
436
+ ]}>
437
+ {t('redux.timeline', 'Timeline')}:
438
+ </Text>
439
+ <Text
440
+ style={[
441
+ reduxDetailStyles.statValue,
442
+ {color: AppColors.brandPurple},
443
+ ]}>
444
+ {sliceActions.length}
445
+ </Text>
446
+ </View>
447
+ {lastAction && (
448
+ <View
449
+ style={[
450
+ reduxDetailStyles.statPill,
451
+ {
452
+ backgroundColor: AppColors.amber100,
453
+ borderColor: AppColors.amber200,
454
+ flexShrink: 1,
455
+ maxWidth: 160,
456
+ },
457
+ ]}>
458
+ <BoltIcon color={AppColors.amber800Warm} size={11} />
459
+ <Text
460
+ style={[
461
+ reduxDetailStyles.statLabel,
462
+ {color: AppColors.amber800Warm},
463
+ ]}>
464
+ {t('redux.last', 'Last')}:
465
+ </Text>
466
+ <Text
467
+ style={[
468
+ reduxDetailStyles.statValue,
469
+ {color: AppColors.amber800Warm, flexShrink: 1},
470
+ ]}
471
+ numberOfLines={1}>
472
+ {lastAction.type}
473
+ </Text>
474
+ </View>
475
+ )}
476
+ </View>
477
+
478
+ {/* Sub-Tabs */}
479
+ <SegmentedTabs
480
+ tabs={sliceTabs}
481
+ activeKey={sliceTab}
482
+ onChange={key => setSliceTab(key as SliceDetailSubTab)}
483
+ style={{marginTop: 10}}
484
+ />
485
+ </View>
486
+
487
+ {/* Search Bar within slice (for Live, Timeline, Persisted) */}
488
+ {sliceTab !== 'metadata' && (
489
+ <View style={reduxDetailStyles.searchContainer}>
490
+ <SearchIcon color={AppColors.grayTextWeak} size={14} />
491
+ <TextInput
492
+ placeholder={
493
+ sliceTab === 'timeline'
494
+ ? 'Search action type, file, or payload...'
495
+ : 'Search keys or values...'
496
+ }
497
+ placeholderTextColor={AppColors.grayTextWeak}
498
+ value={detailSearch}
499
+ onChangeText={setDetailSearch}
500
+ style={reduxDetailStyles.searchInput}
501
+ autoCorrect={false}
502
+ autoCapitalize="none"
503
+ />
504
+ {detailSearch.length > 0 && (
505
+ <Pressable onPress={() => setDetailSearch('')} hitSlop={10}>
506
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
507
+ </Pressable>
508
+ )}
509
+ </View>
510
+ )}
511
+
512
+ {/* Content Body */}
513
+ {sliceTab === 'timeline' ? (
514
+ <ScrollView
515
+ style={{flex: 1}}
516
+ contentContainerStyle={{padding: 12, paddingBottom: 40}}
517
+ keyboardShouldPersistTaps="handled">
518
+ {filteredTimelineActions.length === 0 ? (
519
+ <View style={reduxDetailStyles.emptyBox}>
520
+ <TimelineIcon color={AppColors.grayTextWeak} size={28} />
521
+ <Text style={reduxDetailStyles.emptyBoxText}>
522
+ {detailSearch.trim()
523
+ ? 'No actions matching your search query.'
524
+ : 'No dispatched actions recorded for this slice yet.'}
525
+ </Text>
526
+ </View>
527
+ ) : (
528
+ filteredTimelineActions.map((action, aIdx) => {
529
+ const isExpanded = expandedActionId === action.id;
530
+ const originMeta = getOriginBadge(action.originType);
531
+ const callerBasename = action.callerFile
532
+ ? action.callerFile.split('/').pop()
533
+ : null;
534
+
535
+ return (
536
+ <AnimatedEntrance key={action.id} index={aIdx} distance={6}>
537
+ <TouchableScale
538
+ onPress={() =>
539
+ setExpandedActionId(isExpanded ? null : action.id)
540
+ }
541
+ style={reduxDetailStyles.timelineCard}>
542
+ <View style={reduxDetailStyles.timelineHeader}>
543
+ <View
544
+ style={{
545
+ flexDirection: 'row',
546
+ alignItems: 'center',
547
+ gap: 6,
548
+ flex: 1,
549
+ minWidth: 0,
550
+ flexWrap: 'wrap',
551
+ }}>
552
+ <View style={reduxDetailStyles.actionBadge}>
553
+ <Text style={reduxDetailStyles.actionBadgeText}>
554
+ #{action.id}
555
+ </Text>
556
+ </View>
557
+ {/* Origin Badge */}
558
+ <View
559
+ style={{
560
+ flexDirection: 'row',
561
+ alignItems: 'center',
562
+ gap: 3,
563
+ backgroundColor: originMeta.bg,
564
+ paddingHorizontal: 5,
565
+ paddingVertical: 2,
566
+ borderRadius: 4,
567
+ borderWidth: 1,
568
+ borderColor: originMeta.border,
569
+ }}>
570
+ {originMeta.renderIcon(originMeta.text, 8.5)}
571
+ <Text
572
+ style={{
573
+ fontFamily: AppFonts.interBold,
574
+ fontSize: 8.5,
575
+ color: originMeta.text,
576
+ letterSpacing: 0.3,
577
+ }}>
578
+ {originMeta.label}
579
+ </Text>
580
+ </View>
581
+
582
+ <HighlightText
583
+ text={action.type}
584
+ search={detailSearch}
585
+ style={reduxDetailStyles.actionTitle}
586
+ highlightStyle={reduxDetailStyles.highlight}
587
+ />
588
+ </View>
589
+
590
+ <View style={reduxDetailStyles.timePill}>
591
+ <ClockIcon color={AppColors.purple} size={10} />
592
+ <Text style={reduxDetailStyles.timeText}>
593
+ {action.timestamp}
594
+ </Text>
595
+ </View>
596
+ </View>
597
+
598
+ {/* Trigger caller preview row */}
599
+ {action.callerFile && (
600
+ <View
601
+ style={{
602
+ flexDirection: 'row',
603
+ alignItems: 'center',
604
+ justifyContent: 'space-between',
605
+ marginTop: 6,
606
+ marginBottom: 2,
607
+ paddingHorizontal: 8,
608
+ paddingVertical: 4,
609
+ backgroundColor: AppColors.grayBackground,
610
+ borderRadius: 6,
611
+ borderWidth: 1,
612
+ borderColor: AppColors.dividerColor,
613
+ }}>
614
+ <View
615
+ style={{
616
+ flexDirection: 'row',
617
+ alignItems: 'center',
618
+ gap: 5,
619
+ flex: 1,
620
+ minWidth: 0,
621
+ }}>
622
+ <DocIcon color={AppColors.sky600} size={10} />
623
+ <Text
624
+ style={{
625
+ fontFamily: AppFonts.interMedium,
626
+ fontSize: 9.5,
627
+ color: AppColors.grayTextWeak,
628
+ }}>
629
+ {t('redux.triggeredFrom', 'Triggered From:')}
630
+ </Text>
631
+ <Text
632
+ style={{
633
+ fontFamily: AppFonts.interBold,
634
+ fontSize: 9.5,
635
+ color: AppColors.sky600,
636
+ }}
637
+ numberOfLines={1}>
638
+ {callerBasename}:{action.callerLine || 1}
639
+ </Text>
640
+ </View>
641
+
642
+ <Pressable
643
+ onPress={() =>
644
+ openInVSCode(
645
+ action.callerFile!,
646
+ action.callerLine,
647
+ action.callerCol,
648
+ )
649
+ }
650
+ hitSlop={8}
651
+ style={{
652
+ flexDirection: 'row',
653
+ alignItems: 'center',
654
+ gap: 3,
655
+ backgroundColor: AppColors.sky100,
656
+ paddingHorizontal: 5,
657
+ paddingVertical: 2,
658
+ borderRadius: 4,
659
+ }}>
660
+ <ExternalLinkIcon
661
+ color={AppColors.sky600}
662
+ size={9}
663
+ />
664
+ <Text
665
+ style={{
666
+ fontFamily: AppFonts.interBold,
667
+ fontSize: 8.5,
668
+ color: AppColors.sky600,
669
+ }}>
670
+ {t('redux.openInEditor', 'Open in Editor')}
671
+ </Text>
672
+ </Pressable>
673
+ </View>
674
+ )}
675
+
676
+ {/* Expanded Payload, Diff Viewer & Call Stack */}
677
+ {isExpanded ? (
678
+ <View style={reduxDetailStyles.expandedContent}>
679
+ {/* Call Stack Trace Box */}
680
+ {action.stack ? (
681
+ <View style={{marginBottom: 12}}>
682
+ <Text
683
+ style={reduxDetailStyles.expandedSectionTitle}>
684
+ {t('redux.callStack', 'Call Stack Trace')}:
685
+ </Text>
686
+ <View
687
+ style={{
688
+ backgroundColor: AppColors.grayBackground,
689
+ borderRadius: 8,
690
+ padding: 8,
691
+ borderWidth: 1,
692
+ borderColor: AppColors.dividerColor,
693
+ gap: 4,
694
+ }}>
695
+ {action.stack
696
+ .split('\n')
697
+ .map(l => l.trim())
698
+ .filter(
699
+ l =>
700
+ l.length > 0 &&
701
+ !l.includes('reduxLogger') &&
702
+ !l.includes('inspectorReduxMiddleware'),
703
+ )
704
+ .slice(0, 8)
705
+ .map((frameLine, fIdx) => {
706
+ const parsed = parseStackLine(
707
+ frameLine,
708
+ fIdx === 0,
709
+ );
710
+ const isApp = parsed.frameType === 'app';
711
+ return (
712
+ <Pressable
713
+ key={fIdx}
714
+ onPress={() => {
715
+ if (parsed.fileName !== 'Unknown') {
716
+ openInVSCode(
717
+ parsed.fullPath ||
718
+ parsed.fileName,
719
+ parsed.lineNumber,
720
+ parsed.columnNumber,
721
+ );
722
+ }
723
+ }}
724
+ style={{
725
+ flexDirection: 'row',
726
+ alignItems: 'center',
727
+ justifyContent: 'space-between',
728
+ paddingVertical: 3,
729
+ borderBottomWidth: fIdx < 7 ? 0.5 : 0,
730
+ borderBottomColor:
731
+ AppColors.dividerColor,
732
+ }}>
733
+ <View style={{flex: 1, minWidth: 0}}>
734
+ <Text
735
+ style={{
736
+ fontFamily: isApp
737
+ ? AppFonts.interBold
738
+ : AppFonts.interRegular,
739
+ fontSize: 9.5,
740
+ color: isApp
741
+ ? AppColors.primaryBlack
742
+ : AppColors.grayTextWeak,
743
+ }}
744
+ numberOfLines={1}>
745
+ {parsed.functionName || 'anonymous'}
746
+ </Text>
747
+ <Text
748
+ style={{
749
+ fontFamily: AppFonts.interRegular,
750
+ fontSize: 8.5,
751
+ color: isApp
752
+ ? AppColors.sky600
753
+ : AppColors.grayTextWeak,
754
+ }}
755
+ numberOfLines={1}>
756
+ {parsed.fileName}:
757
+ {parsed.lineNumber || 1}:
758
+ {parsed.columnNumber || 1}
759
+ </Text>
760
+ </View>
761
+ {isApp && (
762
+ <ExternalLinkIcon
763
+ color={AppColors.sky600}
764
+ size={10}
765
+ />
766
+ )}
767
+ </Pressable>
768
+ );
769
+ })}
770
+ </View>
771
+ </View>
772
+ ) : null}
773
+
774
+ {action.payload !== undefined && (
775
+ <View style={{marginBottom: 12}}>
776
+ <Text
777
+ style={reduxDetailStyles.expandedSectionTitle}>
778
+ {t('redux.actionPayload', 'Action Payload:')}
779
+ </Text>
780
+ <JsonViewer
781
+ data={action.payload}
782
+ search={detailSearch}
783
+ forceOpen={true}
784
+ wrap={true}
785
+ />
786
+ </View>
787
+ )}
788
+
789
+ <Text style={reduxDetailStyles.expandedSectionTitle}>
790
+ {t(
791
+ 'redux.stateChangesDiff',
792
+ 'State Changes (Diff):',
793
+ )}
794
+ </Text>
795
+ <DiffViewer
796
+ oldData={
797
+ action.prevState?.[selectedReduxSlice] || {}
798
+ }
799
+ newData={
800
+ action.nextState?.[selectedReduxSlice] || {}
801
+ }
802
+ forceOpen={true}
803
+ />
804
+ </View>
805
+ ) : (
806
+ <View style={reduxDetailStyles.collapsedPrompt}>
807
+ <Text style={reduxDetailStyles.collapsedPromptText}>
808
+ {t(
809
+ 'redux.tapToInspectAction',
810
+ 'Tap to inspect action payload & diff changes',
811
+ )}
812
+ </Text>
813
+ <ForwardChevronIcon
814
+ color={AppColors.grayTextWeak}
815
+ size={12}
816
+ />
817
+ </View>
818
+ )}
819
+ </TouchableScale>
820
+ </AnimatedEntrance>
821
+ );
822
+ })
823
+ )}
824
+ </ScrollView>
825
+ ) : sliceTab === 'metadata' ? (
826
+ <ScrollView
827
+ style={{flex: 1}}
828
+ contentContainerStyle={{padding: 12, paddingBottom: 40}}
829
+ keyboardShouldPersistTaps="handled">
830
+ <View style={reduxDetailStyles.metadataCard}>
831
+ {[
832
+ {label: 'Slice Name', value: selectedReduxSlice},
833
+ {label: 'Root Keys Count', value: `${fieldsCount} keys`},
834
+ {label: 'In-Memory State Size', value: getSize(sliceData)},
835
+ {
836
+ label: 'Persistence Status',
837
+ value: isPersisted
838
+ ? `Persisted (${getSize(persistedData)})`
839
+ : 'Not Persisted (In-Memory)',
840
+ },
841
+ {
842
+ label: 'Dispatched Actions History',
843
+ value: `${sliceActions.length} recorded actions`,
844
+ },
845
+ {
846
+ label: 'Last Dispatched Action',
847
+ value: lastAction ? lastAction.type : 'None',
848
+ },
849
+ {
850
+ label: 'Last Action Timestamp',
851
+ value: lastAction ? lastAction.timestamp : 'Initial',
852
+ },
853
+ ].map((row, rIdx) => (
854
+ <View
855
+ key={rIdx}
856
+ style={[
857
+ reduxDetailStyles.metaRow,
858
+ rIdx === 6 && {borderBottomWidth: 0},
859
+ ]}>
860
+ <Text style={reduxDetailStyles.metaLabel}>{row.label}</Text>
861
+ <Text
862
+ style={reduxDetailStyles.metaValue}
863
+ selectable
864
+ numberOfLines={2}>
865
+ {row.value}
866
+ </Text>
867
+ </View>
868
+ ))}
869
+
870
+ {isPersisted && (
871
+ <TouchableOpacity
872
+ style={reduxDetailStyles.clearPersistBtn}
873
+ onPress={handleClearPersistence}>
874
+ <TrashIcon color={AppColors.errorColor} size={14} />
875
+ <Text style={reduxDetailStyles.clearPersistText}>
876
+ Clear Persisted Storage For This Slice
877
+ </Text>
878
+ </TouchableOpacity>
879
+ )}
880
+ </View>
881
+ </ScrollView>
882
+ ) : sliceTab === 'persisted' ? (
883
+ <View style={{flex: 1, padding: 10}}>
884
+ {isPersisted ? (
885
+ <View style={{flex: 1}}>
886
+ <View style={reduxDetailStyles.persistedInfoCard}>
887
+ <View
888
+ style={{
889
+ flexDirection: 'row',
890
+ alignItems: 'center',
891
+ justifyContent: 'space-between',
892
+ }}>
893
+ <View
894
+ style={{
895
+ flexDirection: 'row',
896
+ alignItems: 'center',
897
+ gap: 6,
898
+ }}>
899
+ <StorageIcon color={AppColors.emerald700} size={14} />
900
+ <Text style={reduxDetailStyles.persistedInfoTitle}>
901
+ Stored in persistent storage ({getSize(persistedData)})
902
+ </Text>
903
+ </View>
904
+ <TouchableOpacity
905
+ style={reduxDetailStyles.clearPersistMiniBtn}
906
+ onPress={handleClearPersistence}>
907
+ <TrashIcon color={AppColors.errorColor} size={12} />
908
+ <Text style={reduxDetailStyles.clearPersistMiniText}>
909
+ Clear
910
+ </Text>
911
+ </TouchableOpacity>
912
+ </View>
913
+ </View>
914
+ <View style={{flex: 1, marginTop: 8}}>
915
+ <JsonViewer
916
+ data={persistedData}
917
+ search={detailSearch}
918
+ forceOpen={detailSearch.length > 0 ? true : undefined}
919
+ mode={viewMode}
920
+ onModeChange={setViewMode}
921
+ fullHeight={true}
922
+ wrap={true}
923
+ />
924
+ </View>
925
+ </View>
926
+ ) : (
927
+ <View style={reduxDetailStyles.emptyBox}>
928
+ <StorageIcon color={AppColors.grayTextWeak} size={32} />
929
+ <Text style={reduxDetailStyles.emptyBoxTitle}>
930
+ Not Persisted in Storage
931
+ </Text>
932
+ <Text style={reduxDetailStyles.emptyBoxText}>
933
+ This slice is not stored in AsyncStorage or redux-persist.
934
+ State exists purely in active JavaScript memory.
935
+ </Text>
936
+ </View>
937
+ )}
938
+ </View>
939
+ ) : (
940
+ /* Live State JsonViewer: Direct flex: 1 with fullHeight to prevent nested scroll conflicts */
941
+ <View style={{flex: 1, padding: 10}}>
942
+ <JsonViewer
943
+ data={sliceData}
944
+ search={detailSearch}
945
+ forceOpen={detailSearch.length > 0 ? true : undefined}
946
+ mode={viewMode}
947
+ onModeChange={setViewMode}
948
+ fullHeight={true}
949
+ wrap={true}
950
+ />
951
+ </View>
952
+ )}
953
+ </View>
954
+ );
955
+ }
956
+
957
+ // ══════════════════════════════════════════════════════════════════════════
958
+ // ── 2. RENDER ACTION DETAIL VIEW (when an action is selected directly) ────
959
+ // ══════════════════════════════════════════════════════════════════════════
960
+ if (selectedReduxAction != null) {
961
+ const originMeta = getOriginBadge(selectedReduxAction.originType);
962
+ const callerBasename = selectedReduxAction.callerFile
963
+ ? selectedReduxAction.callerFile.split('/').pop()
964
+ : null;
965
+
966
+ const actionTabs = [
967
+ {
968
+ key: 'payload',
969
+ label: t('redux.payload', 'Payload'),
970
+ icon: (isActive: boolean) => (
971
+ <CodeBracketsIcon
972
+ color={isActive ? AppColors.white : AppColors.grayText}
973
+ size={12}
974
+ />
975
+ ),
976
+ },
977
+ {
978
+ key: 'diff',
979
+ label: t('redux.diff', 'State Diff'),
980
+ icon: (isActive: boolean) => (
981
+ <TimelineIcon
982
+ color={isActive ? AppColors.white : AppColors.grayText}
983
+ size={12}
984
+ />
985
+ ),
986
+ },
987
+ {
988
+ key: 'stack',
989
+ label: t('redux.callStack', 'Call Stack'),
990
+ icon: (isActive: boolean) => (
991
+ <DocIcon
992
+ color={isActive ? AppColors.white : AppColors.grayText}
993
+ size={12}
994
+ />
995
+ ),
996
+ },
997
+ {
998
+ key: 'raw',
999
+ label: 'Raw Action',
1000
+ icon: (isActive: boolean) => (
1001
+ <LiveStateIcon
1002
+ color={isActive ? AppColors.white : AppColors.grayText}
1003
+ size={12}
1004
+ />
1005
+ ),
1006
+ },
1007
+ ];
1008
+
1009
+ return (
1010
+ <View style={{flex: 1, backgroundColor: AppColors.grayBackground}}>
1011
+ {/* Top Info Bar */}
1012
+ <View style={reduxDetailStyles.infoBar}>
1013
+ <View style={reduxDetailStyles.infoTopRow}>
1014
+ <View
1015
+ style={{
1016
+ flexDirection: 'row',
1017
+ alignItems: 'center',
1018
+ gap: 6,
1019
+ flexWrap: 'wrap',
1020
+ flex: 1,
1021
+ minWidth: 0,
1022
+ }}>
1023
+ <View style={reduxDetailStyles.actionBadge}>
1024
+ <Text style={reduxDetailStyles.actionBadgeText}>
1025
+ #{selectedReduxAction.id}
1026
+ </Text>
1027
+ </View>
1028
+ {/* Origin Badge */}
1029
+ <View
1030
+ style={{
1031
+ flexDirection: 'row',
1032
+ alignItems: 'center',
1033
+ gap: 3,
1034
+ backgroundColor: originMeta.bg,
1035
+ paddingHorizontal: 5,
1036
+ paddingVertical: 2,
1037
+ borderRadius: 4,
1038
+ borderWidth: 1,
1039
+ borderColor: originMeta.border,
1040
+ }}>
1041
+ {originMeta.renderIcon(originMeta.text, 8.5)}
1042
+ <Text
1043
+ style={{
1044
+ fontFamily: AppFonts.interBold,
1045
+ fontSize: 8.5,
1046
+ color: originMeta.text,
1047
+ letterSpacing: 0.3,
1048
+ }}>
1049
+ {originMeta.label}
1050
+ </Text>
1051
+ </View>
1052
+ <Text style={reduxDetailStyles.sliceNameText} numberOfLines={1}>
1053
+ {selectedReduxAction.type}
1054
+ </Text>
1055
+ {selectedReduxAction.timestamp && (
1056
+ <View style={reduxDetailStyles.timePill}>
1057
+ <ClockIcon color={AppColors.purple} size={10} />
1058
+ <Text style={reduxDetailStyles.timeText}>
1059
+ {selectedReduxAction.timestamp}
1060
+ </Text>
1061
+ </View>
1062
+ )}
1063
+ </View>
1064
+
1065
+ {/* Copy Button */}
1066
+ <CopyButton value={() => selectedReduxAction} label="Copy Action" />
1067
+ </View>
1068
+
1069
+ {/* Sub Stats Row */}
1070
+ <View style={reduxDetailStyles.subStatsRow}>
1071
+ {selectedReduxAction.affectedSlices &&
1072
+ selectedReduxAction.affectedSlices.length > 0 && (
1073
+ <View
1074
+ style={[
1075
+ reduxDetailStyles.statPill,
1076
+ {
1077
+ backgroundColor: AppColors.indigo50,
1078
+ borderColor: AppColors.indigo400,
1079
+ },
1080
+ ]}>
1081
+ <LayersIcon color={AppColors.indigo600Alt} size={11} />
1082
+ <Text
1083
+ style={[
1084
+ reduxDetailStyles.statLabel,
1085
+ {color: AppColors.indigo600Alt},
1086
+ ]}>
1087
+ Slices:
1088
+ </Text>
1089
+ <Text
1090
+ style={[
1091
+ reduxDetailStyles.statValue,
1092
+ {color: AppColors.indigo600Alt},
1093
+ ]}>
1094
+ {selectedReduxAction.affectedSlices.join(', ')}
1095
+ </Text>
1096
+ </View>
1097
+ )}
1098
+ <View
1099
+ style={[
1100
+ reduxDetailStyles.statPill,
1101
+ {
1102
+ backgroundColor: AppColors.sky100,
1103
+ borderColor: AppColors.sky400,
1104
+ },
1105
+ ]}>
1106
+ <Text
1107
+ style={[
1108
+ reduxDetailStyles.statLabel,
1109
+ {color: AppColors.sky600},
1110
+ ]}>
1111
+ Payload Size:
1112
+ </Text>
1113
+ <Text
1114
+ style={[
1115
+ reduxDetailStyles.statValue,
1116
+ {color: AppColors.sky600},
1117
+ ]}>
1118
+ {getSize(selectedReduxAction.payload)}
1119
+ </Text>
1120
+ </View>
1121
+ </View>
1122
+
1123
+ {/* Triggered from caller preview row */}
1124
+ {selectedReduxAction.callerFile && (
1125
+ <View
1126
+ style={{
1127
+ flexDirection: 'row',
1128
+ alignItems: 'center',
1129
+ justifyContent: 'space-between',
1130
+ marginTop: 8,
1131
+ paddingHorizontal: 8,
1132
+ paddingVertical: 4,
1133
+ backgroundColor: AppColors.grayBackground,
1134
+ borderRadius: 6,
1135
+ borderWidth: 1,
1136
+ borderColor: AppColors.dividerColor,
1137
+ }}>
1138
+ <View
1139
+ style={{
1140
+ flexDirection: 'row',
1141
+ alignItems: 'center',
1142
+ gap: 5,
1143
+ flex: 1,
1144
+ minWidth: 0,
1145
+ }}>
1146
+ <DocIcon color={AppColors.sky600} size={10} />
1147
+ <Text
1148
+ style={{
1149
+ fontFamily: AppFonts.interMedium,
1150
+ fontSize: 9.5,
1151
+ color: AppColors.grayTextWeak,
1152
+ }}>
1153
+ Triggered from:
1154
+ </Text>
1155
+ <Text
1156
+ style={{
1157
+ fontFamily: AppFonts.interBold,
1158
+ fontSize: 9.5,
1159
+ color: AppColors.sky600,
1160
+ }}
1161
+ numberOfLines={1}>
1162
+ {callerBasename}:{selectedReduxAction.callerLine || 1}
1163
+ </Text>
1164
+ </View>
1165
+
1166
+ <Pressable
1167
+ onPress={() =>
1168
+ openInVSCode(
1169
+ selectedReduxAction.callerFile!,
1170
+ selectedReduxAction.callerLine,
1171
+ selectedReduxAction.callerCol,
1172
+ )
1173
+ }
1174
+ hitSlop={8}
1175
+ style={{
1176
+ flexDirection: 'row',
1177
+ alignItems: 'center',
1178
+ gap: 3,
1179
+ backgroundColor: AppColors.sky100,
1180
+ paddingHorizontal: 5,
1181
+ paddingVertical: 2,
1182
+ borderRadius: 4,
1183
+ }}>
1184
+ <ExternalLinkIcon color={AppColors.sky600} size={9} />
1185
+ <Text
1186
+ style={{
1187
+ fontFamily: AppFonts.interBold,
1188
+ fontSize: 8.5,
1189
+ color: AppColors.sky600,
1190
+ }}>
1191
+ Open in Editor
1192
+ </Text>
1193
+ </Pressable>
1194
+ </View>
1195
+ )}
1196
+
1197
+ {/* Sub-Tabs */}
1198
+ <SegmentedTabs
1199
+ tabs={actionTabs}
1200
+ activeKey={actionTab}
1201
+ onChange={key => setActionTab(key as ActionDetailSubTab)}
1202
+ style={{marginTop: 10}}
1203
+ />
1204
+ </View>
1205
+
1206
+ {/* Content Body */}
1207
+ {actionTab === 'payload' ? (
1208
+ <View style={{flex: 1, padding: 10}}>
1209
+ {selectedReduxAction.payload !== undefined ? (
1210
+ <JsonViewer
1211
+ data={selectedReduxAction.payload}
1212
+ search={detailSearch}
1213
+ forceOpen={true}
1214
+ fullHeight={true}
1215
+ wrap={true}
1216
+ />
1217
+ ) : (
1218
+ <View style={reduxDetailStyles.emptyBox}>
1219
+ <Text style={reduxDetailStyles.emptyBoxText}>
1220
+ This action has no payload dispatched.
1221
+ </Text>
1222
+ </View>
1223
+ )}
1224
+ </View>
1225
+ ) : actionTab === 'diff' ? (
1226
+ <ScrollView
1227
+ style={{flex: 1}}
1228
+ contentContainerStyle={{padding: 12, paddingBottom: 40}}
1229
+ keyboardShouldPersistTaps="handled">
1230
+ <DiffViewer
1231
+ oldData={selectedReduxAction.prevState || {}}
1232
+ newData={selectedReduxAction.nextState || {}}
1233
+ forceOpen={true}
1234
+ />
1235
+ </ScrollView>
1236
+ ) : actionTab === 'stack' ? (
1237
+ <ScrollView
1238
+ style={{flex: 1}}
1239
+ contentContainerStyle={{padding: 12, paddingBottom: 40}}
1240
+ keyboardShouldPersistTaps="handled">
1241
+ {selectedReduxAction.stack ? (
1242
+ <View
1243
+ style={{
1244
+ backgroundColor: AppColors.white,
1245
+ borderRadius: 10,
1246
+ padding: 10,
1247
+ borderWidth: 1,
1248
+ borderColor: AppColors.dividerColor,
1249
+ }}>
1250
+ <Text style={reduxDetailStyles.expandedSectionTitle}>
1251
+ Dispatched from call stack:
1252
+ </Text>
1253
+ {selectedReduxAction.stack
1254
+ .split('\n')
1255
+ .map(l => l.trim())
1256
+ .filter(
1257
+ l =>
1258
+ l.length > 0 &&
1259
+ !l.includes('reduxLogger') &&
1260
+ !l.includes('inspectorReduxMiddleware'),
1261
+ )
1262
+ .map((frameLine, fIdx) => {
1263
+ const parsed = parseStackLine(frameLine, fIdx === 0);
1264
+ const isApp = parsed.frameType === 'app';
1265
+ return (
1266
+ <Pressable
1267
+ key={fIdx}
1268
+ onPress={() => {
1269
+ if (parsed.fileName !== 'Unknown') {
1270
+ openInVSCode(
1271
+ parsed.fullPath || parsed.fileName,
1272
+ parsed.lineNumber,
1273
+ parsed.columnNumber,
1274
+ );
1275
+ }
1276
+ }}
1277
+ style={{
1278
+ flexDirection: 'row',
1279
+ alignItems: 'center',
1280
+ justifyContent: 'space-between',
1281
+ paddingVertical: 5,
1282
+ borderBottomWidth: 0.5,
1283
+ borderBottomColor: AppColors.dividerColor,
1284
+ }}>
1285
+ <View style={{flex: 1, minWidth: 0}}>
1286
+ <Text
1287
+ style={{
1288
+ fontFamily: isApp
1289
+ ? AppFonts.interBold
1290
+ : AppFonts.interRegular,
1291
+ fontSize: 10.5,
1292
+ color: isApp
1293
+ ? AppColors.primaryBlack
1294
+ : AppColors.grayTextWeak,
1295
+ }}
1296
+ numberOfLines={1}>
1297
+ {parsed.functionName || 'anonymous'}
1298
+ </Text>
1299
+ <Text
1300
+ style={{
1301
+ fontFamily: AppFonts.interRegular,
1302
+ fontSize: 9,
1303
+ color: isApp
1304
+ ? AppColors.sky600
1305
+ : AppColors.grayTextWeak,
1306
+ }}
1307
+ numberOfLines={1}>
1308
+ {parsed.fileName}:{parsed.lineNumber || 1}:
1309
+ {parsed.columnNumber || 1}
1310
+ </Text>
1311
+ </View>
1312
+ {isApp && (
1313
+ <ExternalLinkIcon
1314
+ color={AppColors.sky600}
1315
+ size={11}
1316
+ />
1317
+ )}
1318
+ </Pressable>
1319
+ );
1320
+ })}
1321
+ </View>
1322
+ ) : (
1323
+ <View style={reduxDetailStyles.emptyBox}>
1324
+ <Text style={reduxDetailStyles.emptyBoxText}>
1325
+ No call stack trace captured for this action.
1326
+ </Text>
1327
+ </View>
1328
+ )}
1329
+ </ScrollView>
1330
+ ) : (
1331
+ <View style={{flex: 1, padding: 10}}>
1332
+ <JsonViewer
1333
+ data={selectedReduxAction}
1334
+ forceOpen={true}
1335
+ fullHeight={true}
1336
+ wrap={true}
1337
+ />
1338
+ </View>
1339
+ )}
1340
+ </View>
1341
+ );
1342
+ }
1343
+
1344
+ return (
1345
+ <View style={reduxDetailStyles.emptyBox}>
1346
+ <TerminalIcon color={AppColors.purple} size={32} />
1347
+ <Text style={reduxDetailStyles.emptyBoxTitle}>
1348
+ No Redux Slice Selected
1349
+ </Text>
1350
+ <Text style={reduxDetailStyles.emptyBoxText}>
1351
+ Select a slice from the Redux tab to inspect its state and dispatched
1352
+ actions.
1353
+ </Text>
1354
+ </View>
1355
+ );
1356
+ });
1357
+
1358
+ const reduxDetailStyles = StyleSheet.create({
1359
+ infoBar: {
1360
+ backgroundColor: AppColors.white,
1361
+ padding: 12,
1362
+ borderBottomWidth: 1,
1363
+ borderBottomColor: AppColors.dividerColor,
1364
+ },
1365
+ infoTopRow: {
1366
+ flexDirection: 'row',
1367
+ alignItems: 'center',
1368
+ justifyContent: 'space-between',
1369
+ marginBottom: 8,
1370
+ },
1371
+ methodBadge: {
1372
+ paddingHorizontal: 7,
1373
+ paddingVertical: 2.5,
1374
+ borderRadius: 5,
1375
+ },
1376
+ methodBadgeText: {
1377
+ fontFamily: AppFonts.interBold,
1378
+ fontSize: 9.5,
1379
+ color: AppColors.white,
1380
+ letterSpacing: 0.4,
1381
+ },
1382
+ sliceNameText: {
1383
+ fontFamily: AppFonts.interBold,
1384
+ fontSize: 14,
1385
+ color: AppColors.primaryBlack,
1386
+ flexShrink: 1,
1387
+ },
1388
+ persistedChip: {
1389
+ flexDirection: 'row',
1390
+ alignItems: 'center',
1391
+ gap: 3.5,
1392
+ backgroundColor: AppColors.mintGreenBg,
1393
+ paddingHorizontal: 6,
1394
+ paddingVertical: 2,
1395
+ borderRadius: 4,
1396
+ borderWidth: 1,
1397
+ borderColor: AppColors.mintGreenBorder,
1398
+ },
1399
+ persistedChipText: {
1400
+ fontFamily: AppFonts.interBold,
1401
+ fontSize: 9,
1402
+ color: AppColors.mintGreenText,
1403
+ },
1404
+ inMemoryChip: {
1405
+ backgroundColor: AppColors.indigo50,
1406
+ paddingHorizontal: 6,
1407
+ paddingVertical: 2,
1408
+ borderRadius: 4,
1409
+ borderWidth: 1,
1410
+ borderColor: AppColors.blueBorder,
1411
+ },
1412
+ inMemoryChipText: {
1413
+ fontFamily: AppFonts.interBold,
1414
+ fontSize: 9,
1415
+ color: AppColors.indigo600Alt,
1416
+ },
1417
+ subStatsRow: {
1418
+ flexDirection: 'row',
1419
+ alignItems: 'center',
1420
+ gap: 6,
1421
+ flexWrap: 'wrap',
1422
+ },
1423
+ statPill: {
1424
+ flexDirection: 'row',
1425
+ alignItems: 'center',
1426
+ gap: 4,
1427
+ backgroundColor: AppColors.grayBackground,
1428
+ paddingHorizontal: 6,
1429
+ paddingVertical: 2.5,
1430
+ borderRadius: 5,
1431
+ borderWidth: 1,
1432
+ borderColor: AppColors.dividerColor,
1433
+ },
1434
+ statPillText: {
1435
+ fontFamily: AppFonts.interMedium,
1436
+ fontSize: 10,
1437
+ color: AppColors.grayText,
1438
+ },
1439
+ statLabel: {
1440
+ fontFamily: AppFonts.interBold,
1441
+ fontSize: 10,
1442
+ color: AppColors.grayTextWeak,
1443
+ },
1444
+ statValue: {
1445
+ fontFamily: AppFonts.interBold,
1446
+ fontSize: 10.5,
1447
+ color: AppColors.primaryBlack,
1448
+ },
1449
+ searchContainer: {
1450
+ flexDirection: 'row',
1451
+ alignItems: 'center',
1452
+ backgroundColor: AppColors.white,
1453
+ borderRadius: 8,
1454
+ marginHorizontal: 12,
1455
+ marginTop: 10,
1456
+ paddingHorizontal: 10,
1457
+ borderWidth: 1,
1458
+ borderColor: AppColors.dividerColor,
1459
+ height: 36,
1460
+ gap: 8,
1461
+ },
1462
+ searchInput: {
1463
+ flex: 1,
1464
+ fontFamily: AppFonts.interRegular,
1465
+ fontSize: 12,
1466
+ color: AppColors.primaryBlack,
1467
+ padding: 0,
1468
+ },
1469
+ timelineCard: {
1470
+ backgroundColor: AppColors.white,
1471
+ borderRadius: 10,
1472
+ padding: 12,
1473
+ marginBottom: 8,
1474
+ borderWidth: 1,
1475
+ borderColor: AppColors.dividerColor,
1476
+ shadowColor: AppColors.primaryBlack,
1477
+ shadowOffset: {width: 0, height: 1},
1478
+ shadowOpacity: 0.03,
1479
+ shadowRadius: 3,
1480
+ elevation: 1,
1481
+ },
1482
+ timelineHeader: {
1483
+ flexDirection: 'row',
1484
+ alignItems: 'center',
1485
+ gap: 8,
1486
+ },
1487
+ actionBadge: {
1488
+ backgroundColor: AppColors.indigo50,
1489
+ paddingHorizontal: 6,
1490
+ paddingVertical: 2,
1491
+ borderRadius: 4,
1492
+ borderWidth: 1,
1493
+ borderColor: AppColors.blueBorder,
1494
+ },
1495
+ actionBadgeText: {
1496
+ fontFamily: AppFonts.interBold,
1497
+ fontSize: 9.5,
1498
+ color: AppColors.indigo600Alt,
1499
+ },
1500
+ actionTitle: {
1501
+ flex: 1,
1502
+ fontFamily: AppFonts.interBold,
1503
+ fontSize: 12.5,
1504
+ color: AppColors.primaryBlack,
1505
+ },
1506
+ timePill: {
1507
+ flexDirection: 'row',
1508
+ alignItems: 'center',
1509
+ gap: 3,
1510
+ backgroundColor: `${AppColors.purple}14`,
1511
+ paddingHorizontal: 6,
1512
+ paddingVertical: 2,
1513
+ borderRadius: 4,
1514
+ },
1515
+ timeText: {
1516
+ fontFamily: AppFonts.interMedium,
1517
+ fontSize: 10,
1518
+ color: AppColors.purple,
1519
+ },
1520
+ collapsedPrompt: {
1521
+ flexDirection: 'row',
1522
+ alignItems: 'center',
1523
+ justifyContent: 'space-between',
1524
+ marginTop: 8,
1525
+ paddingTop: 6,
1526
+ borderTopWidth: 1,
1527
+ borderTopColor: AppColors.dividerColor,
1528
+ },
1529
+ collapsedPromptText: {
1530
+ fontFamily: AppFonts.interRegular,
1531
+ fontSize: 10.5,
1532
+ color: AppColors.grayTextWeak,
1533
+ },
1534
+ expandedContent: {
1535
+ marginTop: 10,
1536
+ paddingTop: 10,
1537
+ borderTopWidth: 1,
1538
+ borderTopColor: AppColors.dividerColor,
1539
+ },
1540
+ expandedSectionTitle: {
1541
+ fontFamily: AppFonts.interBold,
1542
+ fontSize: 11,
1543
+ color: AppColors.grayTextStrong,
1544
+ marginBottom: 6,
1545
+ },
1546
+ metadataCard: {
1547
+ backgroundColor: AppColors.white,
1548
+ borderRadius: 12,
1549
+ padding: 12,
1550
+ borderWidth: 1,
1551
+ borderColor: AppColors.dividerColor,
1552
+ shadowColor: AppColors.primaryBlack,
1553
+ shadowOffset: {width: 0, height: 1},
1554
+ shadowOpacity: 0.03,
1555
+ shadowRadius: 3,
1556
+ elevation: 1,
1557
+ },
1558
+ metaRow: {
1559
+ flexDirection: 'row',
1560
+ justifyContent: 'space-between',
1561
+ alignItems: 'center',
1562
+ paddingVertical: 10,
1563
+ borderBottomWidth: 1,
1564
+ borderBottomColor: AppColors.dividerColor,
1565
+ },
1566
+ metaLabel: {
1567
+ fontFamily: AppFonts.interMedium,
1568
+ fontSize: 12,
1569
+ color: AppColors.grayTextWeak,
1570
+ },
1571
+ metaValue: {
1572
+ fontFamily: AppFonts.interBold,
1573
+ fontSize: 12,
1574
+ color: AppColors.primaryBlack,
1575
+ maxWidth: '60%',
1576
+ textAlign: 'right',
1577
+ },
1578
+ persistedInfoCard: {
1579
+ backgroundColor: AppColors.mintGreenBg,
1580
+ borderRadius: 8,
1581
+ borderWidth: 1,
1582
+ borderColor: AppColors.mintGreenBorder,
1583
+ padding: 10,
1584
+ },
1585
+ persistedInfoTitle: {
1586
+ fontFamily: AppFonts.interBold,
1587
+ fontSize: 11,
1588
+ color: AppColors.mintGreenText,
1589
+ },
1590
+ clearPersistMiniBtn: {
1591
+ flexDirection: 'row',
1592
+ alignItems: 'center',
1593
+ gap: 3,
1594
+ backgroundColor: AppColors.errorCardBg,
1595
+ paddingHorizontal: 6,
1596
+ paddingVertical: 2.5,
1597
+ borderRadius: 4,
1598
+ },
1599
+ clearPersistMiniText: {
1600
+ fontFamily: AppFonts.interBold,
1601
+ fontSize: 9.5,
1602
+ color: AppColors.errorColor,
1603
+ },
1604
+ clearPersistBtn: {
1605
+ flexDirection: 'row',
1606
+ alignItems: 'center',
1607
+ justifyContent: 'center',
1608
+ gap: 6,
1609
+ backgroundColor: AppColors.errorCardBg,
1610
+ borderWidth: 1,
1611
+ borderColor: AppColors.errorBorder,
1612
+ borderRadius: 8,
1613
+ paddingVertical: 10,
1614
+ marginTop: 14,
1615
+ },
1616
+ clearPersistText: {
1617
+ fontFamily: AppFonts.interBold,
1618
+ fontSize: 12,
1619
+ color: AppColors.errorColor,
1620
+ },
1621
+ emptyBox: {
1622
+ backgroundColor: AppColors.white,
1623
+ borderRadius: 12,
1624
+ padding: 24,
1625
+ alignItems: 'center',
1626
+ justifyContent: 'center',
1627
+ borderWidth: 1,
1628
+ borderColor: AppColors.dividerColor,
1629
+ margin: 12,
1630
+ gap: 8,
1631
+ },
1632
+ emptyBoxTitle: {
1633
+ fontFamily: AppFonts.interBold,
1634
+ fontSize: 14,
1635
+ color: AppColors.primaryBlack,
1636
+ },
1637
+ emptyBoxText: {
1638
+ fontFamily: AppFonts.interMedium,
1639
+ fontSize: 12,
1640
+ color: AppColors.grayTextWeak,
1641
+ textAlign: 'center',
1642
+ lineHeight: 18,
1643
+ },
1644
+ highlight: {
1645
+ backgroundColor: AppColors.yellowHighlight,
1646
+ color: AppColors.amber800Warm,
1647
+ fontFamily: AppFonts.interBold,
1648
+ },
1649
+ });
1650
+
1651
+ export default ReduxDetail;