react-native-inapp-inspector 2.2.1 → 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 (181) hide show
  1. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  2. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  3. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  4. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  5. package/android/.gradle/8.9/executionHistory/executionHistory.lock +0 -0
  6. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  7. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  8. package/android/.gradle/8.9/gc.properties +0 -0
  9. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  10. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  11. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  12. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  13. package/android/.gradle/9.2.0/gc.properties +0 -0
  14. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  15. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  16. package/android/.gradle/vcs-1/gc.properties +0 -0
  17. package/android/build/reports/problems/problems-report.html +659 -0
  18. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +28 -6
  19. package/dist/commonjs/analytics.d.ts +7 -0
  20. package/dist/commonjs/analytics.js +28 -0
  21. package/dist/commonjs/bundle.d.ts +6 -0
  22. package/dist/commonjs/bundle.js +20 -0
  23. package/dist/commonjs/console.d.ts +7 -0
  24. package/dist/commonjs/console.js +20 -0
  25. package/dist/commonjs/constants/version.d.ts +1 -1
  26. package/dist/commonjs/constants/version.js +1 -1
  27. package/dist/commonjs/crash.d.ts +7 -0
  28. package/dist/commonjs/crash.js +27 -0
  29. package/dist/commonjs/customHooks/analyticsLogger.d.ts +3 -0
  30. package/dist/commonjs/customHooks/analyticsLogger.js +23 -2
  31. package/dist/commonjs/customHooks/consoleLogger.d.ts +3 -0
  32. package/dist/commonjs/customHooks/consoleLogger.js +24 -4
  33. package/dist/commonjs/customHooks/crashHandler.d.ts +2 -0
  34. package/dist/commonjs/customHooks/crashHandler.js +15 -1
  35. package/dist/commonjs/customHooks/networkLogger.d.ts +3 -0
  36. package/dist/commonjs/customHooks/networkLogger.js +23 -2
  37. package/dist/commonjs/customHooks/performanceTracker.d.ts +3 -0
  38. package/dist/commonjs/customHooks/performanceTracker.js +34 -3
  39. package/dist/commonjs/customHooks/reduxLogger.d.ts +3 -0
  40. package/dist/commonjs/customHooks/reduxLogger.js +24 -4
  41. package/dist/commonjs/helpers/index.d.ts +1 -0
  42. package/dist/commonjs/helpers/index.js +1 -0
  43. package/dist/commonjs/helpers/memoryManager.d.ts +29 -0
  44. package/dist/commonjs/helpers/memoryManager.js +112 -0
  45. package/dist/commonjs/helpers/telemetry.js +4 -4
  46. package/dist/commonjs/index.d.ts +7 -1
  47. package/dist/commonjs/index.js +36 -2
  48. package/dist/commonjs/network.d.ts +6 -0
  49. package/dist/commonjs/network.js +17 -0
  50. package/dist/commonjs/performance.d.ts +6 -0
  51. package/dist/commonjs/performance.js +33 -0
  52. package/dist/commonjs/redux.d.ts +5 -0
  53. package/dist/commonjs/redux.js +24 -0
  54. package/dist/esm/analytics.d.ts +7 -0
  55. package/dist/esm/analytics.js +6 -0
  56. package/dist/esm/bundle.d.ts +6 -0
  57. package/dist/esm/bundle.js +6 -0
  58. package/dist/esm/console.d.ts +7 -0
  59. package/dist/esm/console.js +6 -0
  60. package/dist/esm/constants/version.d.ts +1 -1
  61. package/dist/esm/constants/version.js +1 -1
  62. package/dist/esm/crash.d.ts +7 -0
  63. package/dist/esm/crash.js +6 -0
  64. package/dist/esm/customHooks/analyticsLogger.d.ts +3 -0
  65. package/dist/esm/customHooks/analyticsLogger.js +19 -1
  66. package/dist/esm/customHooks/consoleLogger.d.ts +3 -0
  67. package/dist/esm/customHooks/consoleLogger.js +20 -3
  68. package/dist/esm/customHooks/crashHandler.d.ts +2 -0
  69. package/dist/esm/customHooks/crashHandler.js +12 -0
  70. package/dist/esm/customHooks/networkLogger.d.ts +3 -0
  71. package/dist/esm/customHooks/networkLogger.js +19 -1
  72. package/dist/esm/customHooks/performanceTracker.d.ts +3 -0
  73. package/dist/esm/customHooks/performanceTracker.js +30 -2
  74. package/dist/esm/customHooks/reduxLogger.d.ts +3 -0
  75. package/dist/esm/customHooks/reduxLogger.js +20 -3
  76. package/dist/esm/helpers/index.d.ts +1 -0
  77. package/dist/esm/helpers/index.js +1 -0
  78. package/dist/esm/helpers/memoryManager.d.ts +29 -0
  79. package/dist/esm/helpers/memoryManager.js +104 -0
  80. package/dist/esm/helpers/telemetry.js +4 -4
  81. package/dist/esm/index.d.ts +7 -1
  82. package/dist/esm/index.js +17 -3
  83. package/dist/esm/network.d.ts +6 -0
  84. package/dist/esm/network.js +5 -0
  85. package/dist/esm/performance.d.ts +6 -0
  86. package/dist/esm/performance.js +6 -0
  87. package/dist/esm/redux.d.ts +5 -0
  88. package/dist/esm/redux.js +5 -0
  89. package/package.json +53 -2
  90. package/src/analytics.ts +35 -0
  91. package/src/bundle.ts +25 -0
  92. package/src/components/AnalyticsDetail.tsx +865 -0
  93. package/src/components/AnalyticsEventCard.tsx +441 -0
  94. package/src/components/AnalyticsGraph.tsx +583 -0
  95. package/src/components/AnimatedEntrance.tsx +64 -0
  96. package/src/components/AppHeaderLogo.tsx +109 -0
  97. package/src/components/BrandCircleIcon.tsx +144 -0
  98. package/src/components/BrandSquareIcon.tsx +144 -0
  99. package/src/components/CodeSnippet.tsx +725 -0
  100. package/src/components/ConsoleLogCard.tsx +628 -0
  101. package/src/components/CopyButton.tsx +87 -0
  102. package/src/components/DiffViewer.tsx +82 -0
  103. package/src/components/DomainHeader.tsx +237 -0
  104. package/src/components/EmptyState.tsx +73 -0
  105. package/src/components/EndOfListFooter.tsx +100 -0
  106. package/src/components/ErrorBoundary.tsx +688 -0
  107. package/src/components/HeadersSection.tsx +192 -0
  108. package/src/components/HighlightText.tsx +100 -0
  109. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  110. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  111. package/src/components/Inspector/BundleTab.tsx +4731 -0
  112. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  113. package/src/components/Inspector/CrashDetail.tsx +941 -0
  114. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  115. package/src/components/Inspector/CrashTab.tsx +871 -0
  116. package/src/components/Inspector/FabLauncher.tsx +80 -0
  117. package/src/components/Inspector/InspectorContext.tsx +28 -0
  118. package/src/components/Inspector/InspectorHeader.tsx +973 -0
  119. package/src/components/Inspector/LogDetail.tsx +1427 -0
  120. package/src/components/Inspector/MainScreen.tsx +258 -0
  121. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  122. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  123. package/src/components/Inspector/NetworkTab.tsx +1095 -0
  124. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  125. package/src/components/Inspector/PerformanceTab.tsx +1894 -0
  126. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  127. package/src/components/Inspector/ReduxTab.tsx +954 -0
  128. package/src/components/Inspector/SettingsPanel.tsx +3181 -0
  129. package/src/components/Inspector/TabBar.tsx +187 -0
  130. package/src/components/Inspector/TelemetryConsentModal.tsx +392 -0
  131. package/src/components/Inspector/UpdateAvailableModal.tsx +545 -0
  132. package/src/components/JsonViewer.tsx +486 -0
  133. package/src/components/LogCard.tsx +491 -0
  134. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  135. package/src/components/MetaAccordion.tsx +340 -0
  136. package/src/components/MiniBarChart.tsx +42 -0
  137. package/src/components/MiniLineChart.tsx +33 -0
  138. package/src/components/NetworkIcons.tsx +2118 -0
  139. package/src/components/SectionHeader.tsx +113 -0
  140. package/src/components/SegmentedTabs.tsx +83 -0
  141. package/src/components/Slider.tsx +300 -0
  142. package/src/components/SourcePageCard.tsx +148 -0
  143. package/src/components/Toast.tsx +131 -0
  144. package/src/components/TouchableScale.tsx +91 -0
  145. package/src/components/TreeNode.tsx +186 -0
  146. package/src/console.ts +24 -0
  147. package/src/constants/index.ts +38 -0
  148. package/src/constants/version.ts +3 -0
  149. package/src/crash.ts +32 -0
  150. package/src/customHooks/analyticsLogger.ts +336 -0
  151. package/src/customHooks/bundleAnalyzer.ts +1231 -0
  152. package/src/customHooks/consoleLogger.ts +497 -0
  153. package/src/customHooks/crashHandler.ts +944 -0
  154. package/src/customHooks/logFilters.ts +32 -0
  155. package/src/customHooks/networkLogger.ts +419 -0
  156. package/src/customHooks/performanceTracker.ts +1014 -0
  157. package/src/customHooks/reduxLogger.ts +406 -0
  158. package/src/customHooks/useAccordion.tsx +60 -0
  159. package/src/decorators/index.ts +184 -0
  160. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  161. package/src/helpers/index.ts +860 -0
  162. package/src/helpers/memoryManager.ts +138 -0
  163. package/src/helpers/searchQueryParser.ts +283 -0
  164. package/src/helpers/settingsStore.ts +171 -0
  165. package/src/helpers/telemetry.ts +505 -0
  166. package/src/helpers/toast.ts +17 -0
  167. package/src/i18n/index.ts +69 -0
  168. package/src/i18n/locales/en.json +1052 -0
  169. package/src/index.tsx +2336 -0
  170. package/src/native/NativeInspector.ts +397 -0
  171. package/src/native/NativeNetworkInspector.ts +24 -0
  172. package/src/network.ts +22 -0
  173. package/src/performance.ts +38 -0
  174. package/src/redux.ts +23 -0
  175. package/src/styles/AppColors.ts +408 -0
  176. package/src/styles/AppFonts.ts +8 -0
  177. package/src/styles/common.ts +209 -0
  178. package/src/styles/index.ts +1570 -0
  179. package/src/types/enums.ts +194 -0
  180. package/src/types/index.ts +34 -0
  181. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,954 @@
1
+ import React, {useCallback, useMemo, useState} from 'react';
2
+ import {
3
+ FlatList,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import {useTranslation} from '../../i18n';
12
+ import {useInspector} from './InspectorContext';
13
+ import TouchableScale from '../TouchableScale';
14
+ import AnimatedEntrance from '../AnimatedEntrance';
15
+ import EmptyState from '../EmptyState';
16
+ import EndOfListFooter from '../EndOfListFooter';
17
+ import HighlightText from '../HighlightText';
18
+ import {getSize, openInVSCode} from '../../helpers';
19
+ import styles from '../../styles';
20
+ import {AppColors} from '../../styles/AppColors';
21
+ import {AppFonts} from '../../styles/AppFonts';
22
+ import {
23
+ getActionHistory,
24
+ getReduxAutoRefresh,
25
+ setReduxAutoRefresh,
26
+ } from '../../customHooks/reduxLogger';
27
+ import {
28
+ TerminalIcon,
29
+ SearchIcon,
30
+ ClearIcon,
31
+ LayersIcon,
32
+ ClockIcon,
33
+ ForwardChevronIcon,
34
+ SortArrowIcon,
35
+ BoltIcon,
36
+ AtomIcon,
37
+ ScreenIcon,
38
+ ListenerIcon,
39
+ DocIcon,
40
+ LoadingSpinnerIcon,
41
+ CircleAlertIcon,
42
+ TextAaIcon,
43
+ StorageIcon,
44
+ HeaderPauseIcon,
45
+ } from '../NetworkIcons';
46
+
47
+ interface ReduxSliceItem {
48
+ id: string;
49
+ name: string;
50
+ keysCount: number;
51
+ sizeStr: string;
52
+ typeLabel: string;
53
+ status: 'live' | 'loading' | 'error' | 'empty';
54
+ statusMessage?: string;
55
+ timelineCount: number;
56
+ lastAction?: {
57
+ type: string;
58
+ timestamp: string;
59
+ updatedAt: number;
60
+ payloadPreview?: string;
61
+ originType?: 'saga' | 'thunk' | 'ui' | 'direct' | 'listener';
62
+ callerFile?: string;
63
+ callerLine?: number;
64
+ callerCol?: number;
65
+ };
66
+ updatedAt: number;
67
+ }
68
+
69
+ const getOriginBadge = (originType?: string) => {
70
+ switch (originType) {
71
+ case 'saga':
72
+ return {
73
+ label: 'SAGA',
74
+ renderIcon: (color: string, size = 9) => <BoltIcon color={color} size={size} />,
75
+ bg: AppColors.purple100,
76
+ text: AppColors.brandPurple,
77
+ border: AppColors.purple200,
78
+ };
79
+ case 'thunk':
80
+ return {
81
+ label: 'THUNK',
82
+ renderIcon: (color: string, size = 9) => <AtomIcon color={color} size={size} />,
83
+ bg: AppColors.amber100,
84
+ text: AppColors.amber800Warm,
85
+ border: AppColors.amber200,
86
+ };
87
+ case 'ui':
88
+ return {
89
+ label: 'UI',
90
+ renderIcon: (color: string, size = 9) => <ScreenIcon color={color} size={size} />,
91
+ bg: AppColors.sky100,
92
+ text: AppColors.sky600,
93
+ border: AppColors.sky400,
94
+ };
95
+ case 'listener':
96
+ return {
97
+ label: 'LISTENER',
98
+ renderIcon: (color: string, size = 9) => <ListenerIcon color={color} size={size} />,
99
+ bg: AppColors.teal100,
100
+ text: AppColors.teal700,
101
+ border: AppColors.teal400,
102
+ };
103
+ default:
104
+ return {
105
+ label: 'DIRECT',
106
+ renderIcon: (color: string, size = 9) => <BoltIcon color={color} size={size} />,
107
+ bg: AppColors.slate100,
108
+ text: AppColors.slate700,
109
+ border: AppColors.slate200,
110
+ };
111
+ }
112
+ };
113
+
114
+ const ReduxTab = React.memo(() => {
115
+ const {t} = useTranslation();
116
+ const {
117
+ reduxState,
118
+ reduxLastActionMap,
119
+ reduxSearch,
120
+ setReduxSearch,
121
+ setSelectedReduxSlice,
122
+ } = useInspector();
123
+
124
+ // Sort mode: 'latest' (newest updated first) vs 'alpha' (A-Z)
125
+ const [sortMode, setSortMode] = useState<'latest' | 'alpha'>('latest');
126
+
127
+ // Pause / Live Auto-Refresh State
128
+ const [isReduxPaused, setIsReduxPaused] = useState<boolean>(
129
+ !getReduxAutoRefresh(),
130
+ );
131
+
132
+ const handleTogglePause = useCallback(() => {
133
+ setIsReduxPaused(prev => {
134
+ const next = !prev;
135
+ setReduxAutoRefresh(!next);
136
+ return next;
137
+ });
138
+ }, []);
139
+
140
+ // Total state size calculation
141
+ const totalStateSize = useMemo(() => {
142
+ return getSize(reduxState);
143
+ }, [reduxState]);
144
+
145
+ // Fetch full action history for timeline counts
146
+ const allActions = useMemo(() => {
147
+ return getActionHistory();
148
+ }, [reduxLastActionMap]);
149
+
150
+ // Compute slice items with exact timestamps & rich details
151
+ const sliceItems: ReduxSliceItem[] = useMemo(() => {
152
+ if (!reduxState || typeof reduxState !== 'object') return [];
153
+ const keys = Object.keys(reduxState);
154
+ return keys.map(key => {
155
+ const sliceVal = reduxState[key];
156
+ const isArray = Array.isArray(sliceVal);
157
+ const isObject = sliceVal && typeof sliceVal === 'object';
158
+
159
+ let typeLabel = 'Primitive';
160
+ let keysCount = 0;
161
+
162
+ if (isArray) {
163
+ keysCount = sliceVal.length;
164
+ typeLabel = `Array [${keysCount}]`;
165
+ } else if (isObject) {
166
+ keysCount = Object.keys(sliceVal).length;
167
+ typeLabel = `Object {${keysCount}}`;
168
+ } else if (typeof sliceVal !== 'undefined') {
169
+ keysCount = 1;
170
+ typeLabel = typeof sliceVal;
171
+ }
172
+
173
+ // Determine state status
174
+ let status: 'live' | 'loading' | 'error' | 'empty' = 'live';
175
+ let statusMessage: string | undefined;
176
+
177
+ if (isObject) {
178
+ if (
179
+ sliceVal.loading === true ||
180
+ sliceVal.isLoading === true ||
181
+ sliceVal.status === 'loading' ||
182
+ sliceVal.status === 'pending'
183
+ ) {
184
+ status = 'loading';
185
+ statusMessage = 'Loading';
186
+ } else if (
187
+ sliceVal.error ||
188
+ sliceVal.hasError ||
189
+ sliceVal.status === 'error' ||
190
+ sliceVal.status === 'failed'
191
+ ) {
192
+ status = 'error';
193
+ statusMessage = typeof sliceVal.error === 'string' ? sliceVal.error : 'Error';
194
+ } else if (keysCount === 0) {
195
+ status = 'empty';
196
+ statusMessage = 'Empty';
197
+ }
198
+ }
199
+
200
+ const lastActionRaw = reduxLastActionMap[key];
201
+ const updatedAt = lastActionRaw?.updatedAt || 0;
202
+
203
+ // Count actions affecting this slice in timeline
204
+ const timelineCount = allActions.filter(action => {
205
+ if (!action.affectedSlices || action.affectedSlices.length === 0) return true;
206
+ return action.affectedSlices.includes(key);
207
+ }).length;
208
+
209
+ let lastAction: ReduxSliceItem['lastAction'] = undefined;
210
+ if (lastActionRaw) {
211
+ let payloadPreview: string | undefined;
212
+ if (lastActionRaw.payload !== undefined && lastActionRaw.payload !== null) {
213
+ try {
214
+ if (typeof lastActionRaw.payload === 'object') {
215
+ const pKeys = Object.keys(lastActionRaw.payload);
216
+ payloadPreview = `{ ${pKeys.slice(0, 3).join(', ')}${pKeys.length > 3 ? '...' : ''} }`;
217
+ } else {
218
+ payloadPreview = String(lastActionRaw.payload);
219
+ }
220
+ } catch {}
221
+ }
222
+
223
+ lastAction = {
224
+ type: lastActionRaw.type,
225
+ timestamp: lastActionRaw.timestamp,
226
+ updatedAt: lastActionRaw.updatedAt,
227
+ payloadPreview,
228
+ originType: lastActionRaw.originType,
229
+ callerFile: lastActionRaw.callerFile,
230
+ callerLine: lastActionRaw.callerLine,
231
+ callerCol: lastActionRaw.callerCol,
232
+ };
233
+ }
234
+
235
+ return {
236
+ id: `slice-${key}`,
237
+ name: key,
238
+ keysCount,
239
+ sizeStr: getSize(sliceVal),
240
+ typeLabel,
241
+ status,
242
+ statusMessage,
243
+ timelineCount,
244
+ lastAction,
245
+ updatedAt,
246
+ };
247
+ });
248
+ }, [reduxState, reduxLastActionMap, allActions]);
249
+
250
+ // Filter & Sort slices: by default, the most recently updated item is at the FIRST position
251
+ const filteredSlices = useMemo(() => {
252
+ let list = sliceItems;
253
+ if (reduxSearch && reduxSearch.trim().length > 0) {
254
+ const queryTokens = reduxSearch.trim().toLowerCase().split(/\s+/).filter(Boolean);
255
+ list = list.filter(item => {
256
+ const sliceVal = reduxState ? reduxState[item.name] : null;
257
+ const searchTarget = [
258
+ item.name || '',
259
+ item.lastAction?.type || '',
260
+ item.lastAction?.payloadPreview || '',
261
+ item.lastAction?.originType || '',
262
+ item.status || '',
263
+ item.statusMessage || '',
264
+ sliceVal ? (typeof sliceVal === 'string' ? sliceVal : JSON.stringify(sliceVal)) : '',
265
+ ].join(' ').toLowerCase();
266
+
267
+ return queryTokens.every(tok => searchTarget.includes(tok));
268
+ });
269
+ }
270
+
271
+ return [...list].sort((a, b) => {
272
+ if (sortMode === 'latest') {
273
+ // Most recently updated item first
274
+ if (b.updatedAt !== a.updatedAt) {
275
+ return b.updatedAt - a.updatedAt;
276
+ }
277
+ return a.name.localeCompare(b.name);
278
+ }
279
+ return a.name.localeCompare(b.name);
280
+ });
281
+ }, [sliceItems, reduxSearch, sortMode, reduxState]);
282
+
283
+ const lastGlobalAction = useMemo(() => {
284
+ const actions = Object.values(reduxLastActionMap);
285
+ if (actions.length === 0) return null;
286
+ return actions.reduce((latest, curr) => {
287
+ if (!latest) return curr;
288
+ return (curr.updatedAt || 0) > (latest.updatedAt || 0) ? curr : latest;
289
+ }, null as any);
290
+ }, [reduxLastActionMap]);
291
+
292
+ const renderSliceItem = useCallback(
293
+ ({item, index}: {item: ReduxSliceItem; index: number}) => {
294
+ const isRecentlyUpdated =
295
+ item.updatedAt > 0 && Date.now() - item.updatedAt < 5000;
296
+
297
+ return (
298
+ <AnimatedEntrance index={index} distance={8}>
299
+ <TouchableScale
300
+ onPress={() => setSelectedReduxSlice(item.name)}
301
+ style={[
302
+ reduxTabStyles.card,
303
+ isRecentlyUpdated && reduxTabStyles.cardRecentlyUpdated,
304
+ ]}>
305
+ {/* Top row: Badge + Slice Name + Status + Timestamp + Chevron */}
306
+ <View style={reduxTabStyles.cardHeader}>
307
+ <View style={reduxTabStyles.sliceBadge}>
308
+ <Text style={reduxTabStyles.sliceBadgeText}>SLICE</Text>
309
+ </View>
310
+
311
+ <HighlightText
312
+ text={item.name}
313
+ search={reduxSearch}
314
+ style={reduxTabStyles.sliceName}
315
+ highlightStyle={reduxTabStyles.highlight}
316
+ />
317
+
318
+ {/* Status Badge */}
319
+ {item.status === 'loading' ? (
320
+ <View style={reduxTabStyles.loadingPill}>
321
+ <LoadingSpinnerIcon color={AppColors.amber800Warm} size={10} />
322
+ <Text style={reduxTabStyles.loadingPillText}>Loading</Text>
323
+ </View>
324
+ ) : item.status === 'error' ? (
325
+ <View style={reduxTabStyles.errorPill}>
326
+ <CircleAlertIcon color={AppColors.red600} size={10} />
327
+ <Text style={reduxTabStyles.errorPillText}>Error</Text>
328
+ </View>
329
+ ) : item.status === 'empty' ? (
330
+ <View style={reduxTabStyles.emptyPill}>
331
+ <Text style={reduxTabStyles.emptyPillText}>Empty</Text>
332
+ </View>
333
+ ) : (
334
+ <View style={reduxTabStyles.livePill}>
335
+ <View style={reduxTabStyles.liveDot} />
336
+ <Text style={reduxTabStyles.livePillText}>Live</Text>
337
+ </View>
338
+ )}
339
+
340
+ {item.lastAction?.timestamp && (
341
+ <View style={reduxTabStyles.timePill}>
342
+ <ClockIcon color={AppColors.purple} size={10} />
343
+ <Text style={reduxTabStyles.timeText}>
344
+ {item.lastAction.timestamp}
345
+ </Text>
346
+ </View>
347
+ )}
348
+
349
+ <ForwardChevronIcon color={AppColors.grayTextWeak} size={13} />
350
+ </View>
351
+
352
+ {/* Middle row: Stats pills */}
353
+ <View style={reduxTabStyles.statsRow}>
354
+ <View style={reduxTabStyles.pill}>
355
+ <LayersIcon color={AppColors.grayTextWeak} size={11} />
356
+ <Text style={reduxTabStyles.pillValue}>{item.typeLabel}</Text>
357
+ </View>
358
+ <View style={reduxTabStyles.pill}>
359
+ <Text style={reduxTabStyles.pillLabel}>Size:</Text>
360
+ <Text style={reduxTabStyles.pillValue}>{item.sizeStr}</Text>
361
+ </View>
362
+ {item.timelineCount > 0 && (
363
+ <View style={reduxTabStyles.pill}>
364
+ <Text style={reduxTabStyles.pillLabel}>Actions:</Text>
365
+ <Text style={reduxTabStyles.pillValue}>{item.timelineCount}</Text>
366
+ </View>
367
+ )}
368
+ </View>
369
+
370
+ {/* Bottom row: Last Dispatched Action with Origin & Trigger Line */}
371
+ {item.lastAction && (
372
+ <View style={reduxTabStyles.lastActionRow}>
373
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 5, flex: 1, minWidth: 0, flexWrap: 'wrap'}}>
374
+ <BoltIcon color={AppColors.brandPurple} size={12} />
375
+ <Text style={reduxTabStyles.lastActionLabel}>{t('redux.last', 'Last')}:</Text>
376
+
377
+ {/* Origin Badge */}
378
+ {item.lastAction.originType && (() => {
379
+ const origin = getOriginBadge(item.lastAction.originType);
380
+ return (
381
+ <View
382
+ style={{
383
+ flexDirection: 'row',
384
+ alignItems: 'center',
385
+ gap: 2.5,
386
+ backgroundColor: origin.bg,
387
+ paddingHorizontal: 4.5,
388
+ paddingVertical: 1.5,
389
+ borderRadius: 3.5,
390
+ borderWidth: 1,
391
+ borderColor: origin.border,
392
+ }}>
393
+ {origin.renderIcon(origin.text, 8.5)}
394
+ <Text
395
+ style={{
396
+ fontFamily: AppFonts.interBold,
397
+ fontSize: 8,
398
+ color: origin.text,
399
+ letterSpacing: 0.3,
400
+ }}>
401
+ {origin.label}
402
+ </Text>
403
+ </View>
404
+ );
405
+ })()}
406
+
407
+ <HighlightText
408
+ text={item.lastAction.type}
409
+ search={reduxSearch}
410
+ style={reduxTabStyles.lastActionValue}
411
+ highlightStyle={reduxTabStyles.highlight}
412
+ numberOfLines={1}
413
+ />
414
+
415
+ {/* Triggered from file preview */}
416
+ {item.lastAction.callerFile && (
417
+ <Pressable
418
+ onPress={() =>
419
+ openInVSCode(
420
+ item.lastAction!.callerFile!,
421
+ item.lastAction!.callerLine,
422
+ item.lastAction!.callerCol,
423
+ )
424
+ }
425
+ hitSlop={6}
426
+ style={{
427
+ flexDirection: 'row',
428
+ alignItems: 'center',
429
+ gap: 2.5,
430
+ backgroundColor: AppColors.sky100,
431
+ paddingHorizontal: 4.5,
432
+ paddingVertical: 1.5,
433
+ borderRadius: 3,
434
+ borderWidth: 0.5,
435
+ borderColor: AppColors.sky400,
436
+ }}>
437
+ <DocIcon color={AppColors.sky600} size={9} />
438
+ <Text
439
+ style={{
440
+ fontFamily: AppFonts.interMedium,
441
+ fontSize: 8.5,
442
+ color: AppColors.sky600,
443
+ }}>
444
+ {item.lastAction.callerFile.split('/').pop()}:{item.lastAction.callerLine || 1}
445
+ </Text>
446
+ </Pressable>
447
+ )}
448
+
449
+ {item.lastAction.payloadPreview && (
450
+ <Text style={reduxTabStyles.payloadPreviewText} numberOfLines={1}>
451
+ {item.lastAction.payloadPreview}
452
+ </Text>
453
+ )}
454
+ </View>
455
+ </View>
456
+ )}
457
+ </TouchableScale>
458
+ </AnimatedEntrance>
459
+ );
460
+ },
461
+ [reduxSearch, setSelectedReduxSlice, t],
462
+ );
463
+
464
+ if (!reduxState) {
465
+ return (
466
+ <View style={styles.emptyContainer}>
467
+ <View style={styles.emptyIconWrap}>
468
+ <TerminalIcon color={AppColors.purple} size={32} />
469
+ </View>
470
+ <Text style={styles.emptyTitle}>No Redux Store Connected</Text>
471
+ <Text style={styles.emptySub}>
472
+ Call connectReduxStore(store) or use inspectorReduxMiddleware at app initialization.
473
+ </Text>
474
+ </View>
475
+ );
476
+ }
477
+
478
+ return (
479
+ <View style={{flex: 1, backgroundColor: AppColors.grayBackground}}>
480
+ {/* ─── Top Stats Overview Card ─── */}
481
+ <View style={reduxTabStyles.overviewCard}>
482
+ <View style={reduxTabStyles.statCol}>
483
+ <Text style={reduxTabStyles.statHeading}>ACTIVE SLICES</Text>
484
+ <Text style={reduxTabStyles.statBigVal}>{sliceItems.length}</Text>
485
+ </View>
486
+ <View style={reduxTabStyles.statDivider} />
487
+ <View style={reduxTabStyles.statCol}>
488
+ <Text style={reduxTabStyles.statHeading}>TOTAL STATE</Text>
489
+ <Text style={reduxTabStyles.statBigVal}>{totalStateSize}</Text>
490
+ </View>
491
+ <View style={reduxTabStyles.statDivider} />
492
+ <View style={[reduxTabStyles.statCol, {flex: 1.6}]}>
493
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
494
+ <Text style={reduxTabStyles.statHeading}>LAST ACTION</Text>
495
+ {lastGlobalAction?.originType && (() => {
496
+ const origin = getOriginBadge(lastGlobalAction.originType);
497
+ return (
498
+ <View
499
+ style={{
500
+ flexDirection: 'row',
501
+ alignItems: 'center',
502
+ gap: 2,
503
+ backgroundColor: origin.bg,
504
+ paddingHorizontal: 4,
505
+ paddingVertical: 1,
506
+ borderRadius: 3,
507
+ }}>
508
+ {origin.renderIcon(origin.text, 8)}
509
+ <Text
510
+ style={{
511
+ fontFamily: AppFonts.interBold,
512
+ fontSize: 7.5,
513
+ color: origin.text,
514
+ }}>
515
+ {origin.label}
516
+ </Text>
517
+ </View>
518
+ );
519
+ })()}
520
+ </View>
521
+ <Text style={reduxTabStyles.statLastAction} numberOfLines={1}>
522
+ {lastGlobalAction ? lastGlobalAction.type : 'Initial'}
523
+ </Text>
524
+ {lastGlobalAction?.timestamp && (
525
+ <Text style={reduxTabStyles.statLastTime}>
526
+ {lastGlobalAction.timestamp}
527
+ </Text>
528
+ )}
529
+ </View>
530
+ </View>
531
+
532
+ {/* ─── Search & Sort Bar ─── */}
533
+ <View style={reduxTabStyles.toolbarContainer}>
534
+ <View style={reduxTabStyles.searchRow}>
535
+ <View style={styles.searchContainer}>
536
+ <SearchIcon color={AppColors.grayTextWeak} size={15} />
537
+ <TextInput
538
+ accessible={true}
539
+ accessibilityRole="search"
540
+ accessibilityLabel="Search Redux slices or actions"
541
+ accessibilityHint="Type to filter state slices in real time"
542
+ placeholder="Search Redux slices or actions..."
543
+ placeholderTextColor={AppColors.grayTextWeak}
544
+ value={reduxSearch}
545
+ onChangeText={setReduxSearch}
546
+ style={styles.searchInput}
547
+ autoCorrect={false}
548
+ autoCapitalize="none"
549
+ />
550
+ {reduxSearch.length > 0 && (
551
+ <Pressable
552
+ accessible={true}
553
+ accessibilityRole="button"
554
+ accessibilityLabel="Clear search input"
555
+ onPress={() => setReduxSearch('')}
556
+ hitSlop={10}>
557
+ <ClearIcon color={AppColors.grayTextWeak} size={14} />
558
+ </Pressable>
559
+ )}
560
+ </View>
561
+
562
+ {/* Pause / Resume Live Updates Button */}
563
+ <TouchableOpacity
564
+ accessible={true}
565
+ accessibilityRole="button"
566
+ accessibilityLabel={
567
+ isReduxPaused
568
+ ? 'Resume live Redux updates'
569
+ : 'Pause live Redux updates'
570
+ }
571
+ accessibilityHint="Toggles automatic recording of Redux state updates"
572
+ style={[
573
+ styles.toolbarBtn,
574
+ isReduxPaused && {
575
+ backgroundColor: `${AppColors.darkOrange}1F`,
576
+ borderColor: AppColors.darkOrange,
577
+ },
578
+ ]}
579
+ onPress={handleTogglePause}
580
+ hitSlop={6}>
581
+ <HeaderPauseIcon
582
+ isPaused={isReduxPaused}
583
+ color={
584
+ isReduxPaused
585
+ ? AppColors.darkOrange
586
+ : AppColors.grayTextStrong
587
+ }
588
+ size={16}
589
+ />
590
+ </TouchableOpacity>
591
+
592
+ {/* Sort Button */}
593
+ <TouchableOpacity
594
+ accessible={true}
595
+ accessibilityRole="button"
596
+ accessibilityLabel={`Sort order: ${
597
+ sortMode === 'latest'
598
+ ? 'Recently updated first'
599
+ : 'Alphabetical A to Z'
600
+ }`}
601
+ accessibilityHint="Switches sorting between recently updated and alphabetical"
602
+ style={[
603
+ styles.toolbarBtn,
604
+ sortMode === 'latest' && {backgroundColor: `${AppColors.brandPurple}1F`, borderColor: AppColors.brandPurple},
605
+ ]}
606
+ onPress={() => setSortMode(prev => (prev === 'latest' ? 'alpha' : 'latest'))}
607
+ hitSlop={6}>
608
+ <SortArrowIcon
609
+ color={sortMode === 'latest' ? AppColors.brandPurple : AppColors.grayTextStrong}
610
+ size={16}
611
+ />
612
+ </TouchableOpacity>
613
+ </View>
614
+
615
+ <View style={reduxTabStyles.headerSubRow}>
616
+ <Text style={reduxTabStyles.resultCount}>
617
+ Showing {filteredSlices.length} of {sliceItems.length} state slices
618
+ </Text>
619
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
620
+ {isReduxPaused && (
621
+ <View style={reduxTabStyles.pausedBannerPill}>
622
+ <Text style={reduxTabStyles.pausedBannerText}>PAUSED</Text>
623
+ </View>
624
+ )}
625
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
626
+ {sortMode === 'latest' ? (
627
+ <BoltIcon color={AppColors.brandPurple} size={11} />
628
+ ) : (
629
+ <TextAaIcon color={AppColors.brandPurple} size={11} />
630
+ )}
631
+ <Text style={reduxTabStyles.sortLabel}>
632
+ {sortMode === 'latest' ? 'Newest Updates First' : 'Alphabetical (A-Z)'}
633
+ </Text>
634
+ </View>
635
+ </View>
636
+ </View>
637
+ </View>
638
+
639
+ {/* ─── FlatList of Slices ─── */}
640
+ <FlatList
641
+ data={filteredSlices}
642
+ keyExtractor={item => item.id}
643
+ renderItem={renderSliceItem}
644
+ initialNumToRender={12}
645
+ maxToRenderPerBatch={8}
646
+ windowSize={5}
647
+ removeClippedSubviews={true}
648
+ renderToHardwareTextureAndroid={true}
649
+ ListEmptyComponent={
650
+ <EmptyState
651
+ isSearch={reduxSearch.length > 0}
652
+ />
653
+ }
654
+ ListFooterComponent={
655
+ filteredSlices.length > 0 ? (
656
+ <EndOfListFooter
657
+ count={filteredSlices.length}
658
+ label="slices"
659
+ />
660
+ ) : null
661
+ }
662
+ contentContainerStyle={[
663
+ reduxTabStyles.listContent,
664
+ filteredSlices.length === 0 && {flexGrow: 1},
665
+ ]}
666
+ keyboardShouldPersistTaps="handled"
667
+ />
668
+ </View>
669
+ );
670
+ });
671
+
672
+ const reduxTabStyles = StyleSheet.create({
673
+ overviewCard: {
674
+ flexDirection: 'row',
675
+ alignItems: 'center',
676
+ backgroundColor: AppColors.white,
677
+ marginHorizontal: 12,
678
+ marginTop: 10,
679
+ borderRadius: 12,
680
+ padding: 12,
681
+ borderWidth: 1,
682
+ borderColor: AppColors.dividerColor,
683
+ shadowColor: AppColors.primaryBlack,
684
+ shadowOffset: {width: 0, height: 1},
685
+ shadowOpacity: 0.03,
686
+ shadowRadius: 3,
687
+ elevation: 1,
688
+ },
689
+ statCol: {
690
+ flex: 1,
691
+ },
692
+ statDivider: {
693
+ width: 1,
694
+ height: 28,
695
+ backgroundColor: AppColors.dividerColor,
696
+ marginHorizontal: 8,
697
+ },
698
+ statHeading: {
699
+ fontFamily: AppFonts.interBold,
700
+ fontSize: 9,
701
+ color: AppColors.grayTextWeak,
702
+ letterSpacing: 0.5,
703
+ },
704
+ statBigVal: {
705
+ fontFamily: AppFonts.interBold,
706
+ fontSize: 14,
707
+ color: AppColors.primaryBlack,
708
+ marginTop: 2,
709
+ },
710
+ statLastAction: {
711
+ fontFamily: AppFonts.interBold,
712
+ fontSize: 11,
713
+ color: AppColors.purple,
714
+ marginTop: 2,
715
+ },
716
+ statLastTime: {
717
+ fontFamily: AppFonts.interRegular,
718
+ fontSize: 9.5,
719
+ color: AppColors.grayTextWeak,
720
+ },
721
+ toolbarContainer: {
722
+ backgroundColor: AppColors.white,
723
+ borderBottomWidth: 1,
724
+ borderBottomColor: AppColors.dividerColor,
725
+ paddingHorizontal: 12,
726
+ paddingTop: 8,
727
+ paddingBottom: 6,
728
+ marginTop: 8,
729
+ },
730
+ searchRow: {
731
+ flexDirection: 'row',
732
+ alignItems: 'center',
733
+ gap: 8,
734
+ },
735
+ headerSubRow: {
736
+ flexDirection: 'row',
737
+ justifyContent: 'space-between',
738
+ alignItems: 'center',
739
+ marginTop: 6,
740
+ paddingHorizontal: 2,
741
+ },
742
+ resultCount: {
743
+ fontFamily: AppFonts.interMedium,
744
+ fontSize: 11,
745
+ color: AppColors.grayTextWeak,
746
+ },
747
+ sortLabel: {
748
+ fontFamily: AppFonts.interMedium,
749
+ fontSize: 10.5,
750
+ color: AppColors.brandPurple,
751
+ },
752
+ listContent: {
753
+ padding: 12,
754
+ paddingBottom: 36,
755
+ },
756
+ card: {
757
+ backgroundColor: AppColors.white,
758
+ borderRadius: 12,
759
+ padding: 12,
760
+ marginBottom: 10,
761
+ borderWidth: 1,
762
+ borderColor: AppColors.dividerColor,
763
+ shadowColor: AppColors.primaryBlack,
764
+ shadowOffset: {width: 0, height: 1},
765
+ shadowOpacity: 0.04,
766
+ shadowRadius: 4,
767
+ elevation: 2,
768
+ },
769
+ cardRecentlyUpdated: {
770
+ borderColor: `${AppColors.brandPurple}66`,
771
+ backgroundColor: AppColors.purpleTintBg,
772
+ },
773
+ cardHeader: {
774
+ flexDirection: 'row',
775
+ alignItems: 'center',
776
+ gap: 8,
777
+ marginBottom: 8,
778
+ },
779
+ sliceBadge: {
780
+ backgroundColor: AppColors.violetSoftBg,
781
+ paddingHorizontal: 6,
782
+ paddingVertical: 3,
783
+ borderRadius: 5,
784
+ borderWidth: 1,
785
+ borderColor: AppColors.violetSoftBorder,
786
+ },
787
+ sliceBadgeText: {
788
+ fontFamily: AppFonts.interBold,
789
+ fontSize: 9.5,
790
+ color: AppColors.purple700,
791
+ letterSpacing: 0.4,
792
+ },
793
+ sliceName: {
794
+ flex: 1,
795
+ fontFamily: AppFonts.interBold,
796
+ fontSize: 13.5,
797
+ color: AppColors.primaryBlack,
798
+ },
799
+ timePill: {
800
+ flexDirection: 'row',
801
+ alignItems: 'center',
802
+ gap: 3,
803
+ backgroundColor: `${AppColors.purple}14`,
804
+ paddingHorizontal: 6,
805
+ paddingVertical: 2,
806
+ borderRadius: 4,
807
+ borderWidth: 1,
808
+ borderColor: `${AppColors.purple}2E`,
809
+ },
810
+ timeText: {
811
+ fontFamily: AppFonts.interMedium,
812
+ fontSize: 10,
813
+ color: AppColors.purple,
814
+ },
815
+ statsRow: {
816
+ flexDirection: 'row',
817
+ alignItems: 'center',
818
+ gap: 6,
819
+ flexWrap: 'wrap',
820
+ marginBottom: 6,
821
+ },
822
+ pill: {
823
+ flexDirection: 'row',
824
+ alignItems: 'center',
825
+ gap: 4,
826
+ backgroundColor: AppColors.grayBackground,
827
+ paddingHorizontal: 7,
828
+ paddingVertical: 3,
829
+ borderRadius: 5,
830
+ borderWidth: 1,
831
+ borderColor: AppColors.dividerColor,
832
+ },
833
+ pillLabel: {
834
+ fontFamily: AppFonts.interMedium,
835
+ fontSize: 10.5,
836
+ color: AppColors.grayTextWeak,
837
+ },
838
+ pillValue: {
839
+ fontFamily: AppFonts.interBold,
840
+ fontSize: 10.5,
841
+ color: AppColors.primaryBlack,
842
+ },
843
+ livePill: {
844
+ flexDirection: 'row',
845
+ alignItems: 'center',
846
+ gap: 4,
847
+ backgroundColor: AppColors.emeraldBg,
848
+ paddingHorizontal: 6,
849
+ paddingVertical: 3,
850
+ borderRadius: 5,
851
+ borderWidth: 1,
852
+ borderColor: AppColors.emeraldBorder,
853
+ },
854
+ liveDot: {
855
+ width: 6,
856
+ height: 6,
857
+ borderRadius: 3,
858
+ backgroundColor: AppColors.emerald500,
859
+ },
860
+ livePillText: {
861
+ fontFamily: AppFonts.interBold,
862
+ fontSize: 10,
863
+ color: AppColors.emerald700,
864
+ },
865
+ loadingPill: {
866
+ backgroundColor: AppColors.amber100,
867
+ paddingHorizontal: 6,
868
+ paddingVertical: 3,
869
+ borderRadius: 5,
870
+ borderWidth: 1,
871
+ borderColor: AppColors.amber200,
872
+ },
873
+ loadingPillText: {
874
+ fontFamily: AppFonts.interBold,
875
+ fontSize: 9.5,
876
+ color: AppColors.amber800Warm,
877
+ },
878
+ errorPill: {
879
+ backgroundColor: AppColors.red100,
880
+ paddingHorizontal: 6,
881
+ paddingVertical: 3,
882
+ borderRadius: 5,
883
+ borderWidth: 1,
884
+ borderColor: AppColors.errorBorder,
885
+ },
886
+ errorPillText: {
887
+ fontFamily: AppFonts.interBold,
888
+ fontSize: 9.5,
889
+ color: AppColors.red600,
890
+ },
891
+ emptyPill: {
892
+ backgroundColor: AppColors.grayBackground,
893
+ paddingHorizontal: 6,
894
+ paddingVertical: 3,
895
+ borderRadius: 5,
896
+ borderWidth: 1,
897
+ borderColor: AppColors.dividerColor,
898
+ },
899
+ emptyPillText: {
900
+ fontFamily: AppFonts.interMedium,
901
+ fontSize: 9.5,
902
+ color: AppColors.grayTextWeak,
903
+ },
904
+ lastActionRow: {
905
+ flexDirection: 'row',
906
+ alignItems: 'center',
907
+ gap: 5,
908
+ backgroundColor: AppColors.grayBackground,
909
+ borderRadius: 6,
910
+ paddingHorizontal: 8,
911
+ paddingVertical: 5,
912
+ marginTop: 4,
913
+ borderWidth: 1,
914
+ borderColor: AppColors.dividerColor,
915
+ },
916
+ lastActionLabel: {
917
+ fontFamily: AppFonts.interMedium,
918
+ fontSize: 10.5,
919
+ color: AppColors.grayTextWeak,
920
+ },
921
+ lastActionValue: {
922
+ fontFamily: AppFonts.interBold,
923
+ fontSize: 11,
924
+ color: AppColors.primaryBlack,
925
+ },
926
+ payloadPreviewText: {
927
+ flex: 1,
928
+ fontFamily: AppFonts.interRegular,
929
+ fontSize: 10,
930
+ color: AppColors.grayTextWeak,
931
+ marginLeft: 4,
932
+ },
933
+ highlight: {
934
+ backgroundColor: AppColors.yellow200,
935
+ color: AppColors.yellow800,
936
+ fontFamily: AppFonts.interBold,
937
+ },
938
+ pausedBannerPill: {
939
+ backgroundColor: AppColors.amber100,
940
+ paddingHorizontal: 6,
941
+ paddingVertical: 2,
942
+ borderRadius: 4,
943
+ borderWidth: 1,
944
+ borderColor: AppColors.amber200,
945
+ },
946
+ pausedBannerText: {
947
+ fontFamily: AppFonts.interBold,
948
+ fontSize: 9,
949
+ color: AppColors.amber800Warm,
950
+ letterSpacing: 0.5,
951
+ },
952
+ });
953
+
954
+ export default ReduxTab;