react-native-inapp-inspector 2.2.2 → 2.2.4

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 (159) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +5 -1
  2. package/dist/commonjs/components/AnalyticsEventCard.js +2 -1
  3. package/dist/commonjs/components/DiffViewer.js +1 -1
  4. package/dist/commonjs/components/Inspector/BundleTab.js +22 -0
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.d.ts +3 -0
  6. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +835 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -14
  8. package/dist/commonjs/components/Inspector/LogDetail.js +6 -3
  9. package/dist/commonjs/components/Inspector/MainScreen.js +7 -3
  10. package/dist/commonjs/components/Inspector/NetworkTab.js +92 -15
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +38 -12
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +440 -229
  13. package/dist/commonjs/components/Inspector/StorageTab.d.ts +3 -0
  14. package/dist/commonjs/components/Inspector/StorageTab.js +892 -0
  15. package/dist/commonjs/components/Inspector/TabBar.js +14 -0
  16. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +11 -2
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +4 -1
  18. package/dist/commonjs/components/NetworkIcons.d.ts +6 -0
  19. package/dist/commonjs/components/NetworkIcons.js +30 -1
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/customHooks/bundleAnalyzer.js +44 -22
  23. package/dist/commonjs/customHooks/storageInspector.d.ts +67 -0
  24. package/dist/commonjs/customHooks/storageInspector.js +486 -0
  25. package/dist/commonjs/i18n/locales/en.json +8 -2
  26. package/dist/commonjs/index.d.ts +1 -0
  27. package/dist/commonjs/index.js +16 -1
  28. package/dist/commonjs/storage.d.ts +5 -0
  29. package/dist/commonjs/storage.js +18 -0
  30. package/dist/commonjs/types/enums.d.ts +5 -0
  31. package/dist/commonjs/types/enums.js +5 -0
  32. package/dist/esm/components/AnalyticsDetail.js +6 -2
  33. package/dist/esm/components/AnalyticsEventCard.js +2 -1
  34. package/dist/esm/components/DiffViewer.js +1 -1
  35. package/dist/esm/components/Inspector/BundleTab.js +22 -0
  36. package/dist/esm/components/Inspector/DeviceInfoTab.d.ts +3 -0
  37. package/dist/esm/components/Inspector/DeviceInfoTab.js +796 -0
  38. package/dist/esm/components/Inspector/InspectorHeader.js +13 -15
  39. package/dist/esm/components/Inspector/LogDetail.js +7 -4
  40. package/dist/esm/components/Inspector/MainScreen.js +7 -3
  41. package/dist/esm/components/Inspector/NetworkTab.js +93 -16
  42. package/dist/esm/components/Inspector/PerformanceTab.js +39 -13
  43. package/dist/esm/components/Inspector/SettingsPanel.js +441 -230
  44. package/dist/esm/components/Inspector/StorageTab.d.ts +3 -0
  45. package/dist/esm/components/Inspector/StorageTab.js +853 -0
  46. package/dist/esm/components/Inspector/TabBar.js +15 -1
  47. package/dist/esm/components/Inspector/TelemetryConsentModal.js +11 -2
  48. package/dist/esm/components/Inspector/UpdateAvailableModal.js +4 -1
  49. package/dist/esm/components/NetworkIcons.d.ts +6 -0
  50. package/dist/esm/components/NetworkIcons.js +23 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/bundleAnalyzer.js +44 -22
  54. package/dist/esm/customHooks/storageInspector.d.ts +67 -0
  55. package/dist/esm/customHooks/storageInspector.js +469 -0
  56. package/dist/esm/i18n/locales/en.json +8 -2
  57. package/dist/esm/index.d.ts +1 -0
  58. package/dist/esm/index.js +5 -0
  59. package/dist/esm/storage.d.ts +5 -0
  60. package/dist/esm/storage.js +5 -0
  61. package/dist/esm/types/enums.d.ts +5 -0
  62. package/dist/esm/types/enums.js +5 -0
  63. package/package.json +9 -1
  64. package/src/analytics.ts +35 -0
  65. package/src/bundle.ts +25 -0
  66. package/src/components/AnalyticsDetail.tsx +870 -0
  67. package/src/components/AnalyticsEventCard.tsx +442 -0
  68. package/src/components/AnalyticsGraph.tsx +583 -0
  69. package/src/components/AnimatedEntrance.tsx +64 -0
  70. package/src/components/AppHeaderLogo.tsx +109 -0
  71. package/src/components/BrandCircleIcon.tsx +144 -0
  72. package/src/components/BrandSquareIcon.tsx +144 -0
  73. package/src/components/CodeSnippet.tsx +725 -0
  74. package/src/components/ConsoleLogCard.tsx +628 -0
  75. package/src/components/CopyButton.tsx +87 -0
  76. package/src/components/DiffViewer.tsx +82 -0
  77. package/src/components/DomainHeader.tsx +237 -0
  78. package/src/components/EmptyState.tsx +73 -0
  79. package/src/components/EndOfListFooter.tsx +100 -0
  80. package/src/components/ErrorBoundary.tsx +688 -0
  81. package/src/components/HeadersSection.tsx +192 -0
  82. package/src/components/HighlightText.tsx +100 -0
  83. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  84. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  85. package/src/components/Inspector/BundleTab.tsx +4760 -0
  86. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  87. package/src/components/Inspector/CrashDetail.tsx +941 -0
  88. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  89. package/src/components/Inspector/CrashTab.tsx +871 -0
  90. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  91. package/src/components/Inspector/FabLauncher.tsx +80 -0
  92. package/src/components/Inspector/InspectorContext.tsx +28 -0
  93. package/src/components/Inspector/InspectorHeader.tsx +972 -0
  94. package/src/components/Inspector/LogDetail.tsx +1431 -0
  95. package/src/components/Inspector/MainScreen.tsx +262 -0
  96. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  97. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  98. package/src/components/Inspector/NetworkTab.tsx +1183 -0
  99. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  100. package/src/components/Inspector/PerformanceTab.tsx +1928 -0
  101. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  102. package/src/components/Inspector/ReduxTab.tsx +954 -0
  103. package/src/components/Inspector/SettingsPanel.tsx +3447 -0
  104. package/src/components/Inspector/StorageTab.tsx +1048 -0
  105. package/src/components/Inspector/TabBar.tsx +207 -0
  106. package/src/components/Inspector/TelemetryConsentModal.tsx +416 -0
  107. package/src/components/Inspector/UpdateAvailableModal.tsx +557 -0
  108. package/src/components/JsonViewer.tsx +486 -0
  109. package/src/components/LogCard.tsx +491 -0
  110. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  111. package/src/components/MetaAccordion.tsx +340 -0
  112. package/src/components/MiniBarChart.tsx +42 -0
  113. package/src/components/MiniLineChart.tsx +33 -0
  114. package/src/components/NetworkIcons.tsx +2221 -0
  115. package/src/components/SectionHeader.tsx +113 -0
  116. package/src/components/SegmentedTabs.tsx +83 -0
  117. package/src/components/Slider.tsx +300 -0
  118. package/src/components/SourcePageCard.tsx +148 -0
  119. package/src/components/Toast.tsx +131 -0
  120. package/src/components/TouchableScale.tsx +91 -0
  121. package/src/components/TreeNode.tsx +186 -0
  122. package/src/console.ts +24 -0
  123. package/src/constants/index.ts +38 -0
  124. package/src/constants/version.ts +3 -0
  125. package/src/crash.ts +32 -0
  126. package/src/customHooks/analyticsLogger.ts +336 -0
  127. package/src/customHooks/bundleAnalyzer.ts +1256 -0
  128. package/src/customHooks/consoleLogger.ts +497 -0
  129. package/src/customHooks/crashHandler.ts +944 -0
  130. package/src/customHooks/logFilters.ts +32 -0
  131. package/src/customHooks/networkLogger.ts +419 -0
  132. package/src/customHooks/performanceTracker.ts +1014 -0
  133. package/src/customHooks/reduxLogger.ts +406 -0
  134. package/src/customHooks/storageInspector.ts +509 -0
  135. package/src/customHooks/useAccordion.tsx +60 -0
  136. package/src/decorators/index.ts +184 -0
  137. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  138. package/src/helpers/index.ts +860 -0
  139. package/src/helpers/memoryManager.ts +138 -0
  140. package/src/helpers/searchQueryParser.ts +283 -0
  141. package/src/helpers/settingsStore.ts +171 -0
  142. package/src/helpers/telemetry.ts +505 -0
  143. package/src/helpers/toast.ts +17 -0
  144. package/src/i18n/index.ts +69 -0
  145. package/src/i18n/locales/en.json +1058 -0
  146. package/src/index.tsx +2356 -0
  147. package/src/native/NativeInspector.ts +397 -0
  148. package/src/native/NativeNetworkInspector.ts +24 -0
  149. package/src/network.ts +22 -0
  150. package/src/performance.ts +38 -0
  151. package/src/redux.ts +23 -0
  152. package/src/storage.ts +18 -0
  153. package/src/styles/AppColors.ts +408 -0
  154. package/src/styles/AppFonts.ts +8 -0
  155. package/src/styles/common.ts +209 -0
  156. package/src/styles/index.ts +1570 -0
  157. package/src/types/enums.ts +199 -0
  158. package/src/types/index.ts +34 -0
  159. 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;