react-native-inapp-inspector 1.0.3 → 1.0.5

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 (47) hide show
  1. package/README.md +3 -3
  2. package/dist/commonjs/components/AnalyticsEventCard.js +10 -10
  3. package/dist/commonjs/components/CodeSnippet.js +233 -10
  4. package/dist/commonjs/components/ConsoleLogCard.js +55 -9
  5. package/dist/commonjs/components/CopyButton.js +2 -1
  6. package/dist/commonjs/components/ErrorBoundary.d.ts +20 -0
  7. package/dist/commonjs/components/ErrorBoundary.js +332 -0
  8. package/dist/commonjs/components/NetworkIcons.d.ts +5 -0
  9. package/dist/commonjs/components/NetworkIcons.js +45 -1
  10. package/dist/commonjs/customHooks/reduxLogger.d.ts +4 -0
  11. package/dist/commonjs/customHooks/reduxLogger.js +30 -0
  12. package/dist/commonjs/customHooks/webViewLogger.d.ts +2 -0
  13. package/dist/commonjs/customHooks/webViewLogger.js +281 -246
  14. package/dist/commonjs/helpers/index.js +2 -1
  15. package/dist/commonjs/index.d.ts +5 -3
  16. package/dist/commonjs/index.js +685 -911
  17. package/dist/commonjs/styles/AppColors.d.ts +29 -1
  18. package/dist/commonjs/styles/AppColors.js +38 -2
  19. package/dist/commonjs/styles/index.d.ts +438 -229
  20. package/dist/commonjs/styles/index.js +448 -209
  21. package/dist/commonjs/types/index.d.ts +2 -2
  22. package/dist/esm/components/AnalyticsEventCard.js +10 -10
  23. package/dist/esm/components/CodeSnippet.js +232 -12
  24. package/dist/esm/components/ConsoleLogCard.js +55 -9
  25. package/dist/esm/components/CopyButton.js +2 -1
  26. package/dist/esm/components/ErrorBoundary.d.ts +20 -0
  27. package/dist/esm/components/ErrorBoundary.js +295 -0
  28. package/dist/esm/components/NetworkIcons.d.ts +5 -0
  29. package/dist/esm/components/NetworkIcons.js +39 -0
  30. package/dist/esm/customHooks/reduxLogger.d.ts +4 -0
  31. package/dist/esm/customHooks/reduxLogger.js +23 -0
  32. package/dist/esm/customHooks/webViewLogger.d.ts +2 -0
  33. package/dist/esm/customHooks/webViewLogger.js +281 -246
  34. package/dist/esm/helpers/index.js +2 -1
  35. package/dist/esm/index.d.ts +5 -3
  36. package/dist/esm/index.js +683 -914
  37. package/dist/esm/styles/AppColors.d.ts +29 -1
  38. package/dist/esm/styles/AppColors.js +35 -1
  39. package/dist/esm/styles/index.d.ts +438 -229
  40. package/dist/esm/styles/index.js +412 -209
  41. package/dist/esm/types/index.d.ts +2 -2
  42. package/example/App.tsx +351 -127
  43. package/example/ios/Podfile.lock +26 -0
  44. package/example/metro.config.js +1 -1
  45. package/example/package-lock.json +20 -4
  46. package/example/package.json +4 -3
  47. package/package.json +1 -1
package/dist/esm/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
2
- import { Alert, Animated, StyleSheet, FlatList, SectionList, Modal, Platform, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, } from 'react-native';
2
+ import { Alert, Animated, StyleSheet, FlatList, Modal, Pressable, ScrollView, Text, TextInput, View, Linking, Image, InteractionManager, ActivityIndicator, StatusBar, TouchableOpacity, } from 'react-native';
3
3
  import Svg, { Circle, Path } from 'react-native-svg';
4
4
  import LinearGradient from 'react-native-linear-gradient';
5
5
  import { useNavigationState, NavigationContext } from '@react-navigation/native';
@@ -8,8 +8,6 @@ import TouchableScale from './components/TouchableScale';
8
8
  import useAccordion from './customHooks/useAccordion';
9
9
  import MetaAccordion from './components/MetaAccordion';
10
10
  import CopyButton from './components/CopyButton';
11
- import MiniBarChart from './components/MiniBarChart';
12
- import MiniLineChart from './components/MiniLineChart';
13
11
  import SectionHeader from './components/SectionHeader';
14
12
  import EmptyState from './components/EmptyState';
15
13
  import JsonViewer from './components/JsonViewer';
@@ -22,13 +20,14 @@ import { ConsoleLogCard } from './components/ConsoleLogCard';
22
20
  import HighlightText from './components/HighlightText';
23
21
  import CodeSnippet from './components/CodeSnippet';
24
22
  // Helpers
25
- import { formatDateTime, getStatusColor, getDurationColor, getNavigationInfo, deduplicateLogs, getDomainColor, formatDisplayUrl, getFetchCommand, getCurlCommand, getSize, } from './helpers';
23
+ import { formatDateTime, getStatusColor, getNavigationInfo, deduplicateLogs, getDomainColor, formatDisplayUrl, getFetchCommand, getCurlCommand, getSize, } from './helpers';
26
24
  // Assets
27
- import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, MapPinIcon, GlobeIcon, DownloadIcon, ExpandCollapseIcon, ChevronIcon, CloseWhite, TrashIcon, WhiteBackNavigation, } from './components/NetworkIcons';
25
+ import { EmptyRadarIcon, FailIcon, SearchIcon, ScreenIcon, ClearIcon, SortArrowIcon, FilterIcon, InsightsIcon, GlobeIcon, DownloadIcon, ChevronIcon, CloseWhite, TrashIcon, WhiteBackNavigation, TerminalIcon, SignalIcon, AnalyticsIcon, SunIcon, MoonIcon, DebugIcon, } from './components/NetworkIcons';
26
+ import ErrorBoundary from './components/ErrorBoundary';
28
27
  // Stylesheet
29
28
  import { AppColors } from './styles/AppColors';
30
29
  import { AppFonts } from './styles/AppFonts';
31
- import styles from './styles';
30
+ import styles, { toggleGlobalTheme } from './styles';
32
31
  // Network
33
32
  import { setupNetworkLogger, clearNetworkLogs, subscribeNetworkLogs, } from './customHooks/networkLogger';
34
33
  // Console
@@ -39,6 +38,7 @@ import AnalyticsEventCard, { getEventColor, } from './components/AnalyticsEventC
39
38
  import AnalyticsDetail from './components/AnalyticsDetail';
40
39
  // WebView
41
40
  import { getWebViewLogs, getWebViewNavHistory, getWebViewHtml, getWebViewCss, getWebViewJs, getWebViewHtmlUrl, clearWebViewData, subscribeWebView, } from './customHooks/webViewLogger';
41
+ import { getReduxState, subscribeReduxState, } from './customHooks/reduxLogger';
42
42
  import { METHOD_COLORS, STATUS_FILTERS } from './constants';
43
43
  const NavigationTracker = ({ onStateChange }) => {
44
44
  const navState = useNavigationState(state => state);
@@ -48,6 +48,9 @@ const NavigationTracker = ({ onStateChange }) => {
48
48
  return null;
49
49
  };
50
50
  const NetworkInspector = () => {
51
+ const [isDark, setIsDark] = useState(false);
52
+ const [reduxState, setReduxState] = useState(null);
53
+ const [expandedReducers, setExpandedReducers] = useState({});
51
54
  const [logs, setLogs] = useState([]);
52
55
  const [visible, setVisible] = useState(false);
53
56
  const [isReady, setIsReady] = useState(false);
@@ -55,6 +58,14 @@ const NetworkInspector = () => {
55
58
  const [selectedLogs, setSelectedLogs] = useState(new Set());
56
59
  const [search, setSearch] = useState('');
57
60
  const [detailSearch, setDetailSearch] = useState('');
61
+ const [reduxSearch, setReduxSearch] = useState('');
62
+ const [apiDetailActiveTab, setApiDetailActiveTab] = useState('metadata');
63
+ useEffect(() => {
64
+ if (selected) {
65
+ setApiDetailActiveTab('metadata');
66
+ setDetailSearch('');
67
+ }
68
+ }, [selected]);
58
69
  const [statusFilters, setStatusFilters] = useState(new Set());
59
70
  const [methodFilters, setMethodFilters] = useState(new Set());
60
71
  const [sectionFilters, setSectionFilters] = useState({});
@@ -96,20 +107,23 @@ const NetworkInspector = () => {
96
107
  const [webViewNavHistory, setWebViewNavHistory] = useState([]);
97
108
  const [webViewSubTab, setWebViewSubTab] = useState('html');
98
109
  const [webViewSearch, setWebViewSearch] = useState('');
99
- const [htmlSearch, setHtmlSearch] = useState('');
100
- const [cssSearch, setCssSearch] = useState('');
101
- const [jsSearch, setJsSearch] = useState('');
102
110
  const [webViewHtml, setWebViewHtml] = useState('');
103
111
  const [webViewCss, setWebViewCss] = useState('');
104
112
  const [webViewJs, setWebViewJs] = useState('');
105
113
  const [webViewHtmlUrl, setWebViewHtmlUrl] = useState('');
106
114
  const [htmlSubTab, setHtmlSubTab] = useState('html');
115
+ const [isHtmlTabReady, setIsHtmlTabReady] = useState(true);
116
+ useEffect(() => {
117
+ setIsHtmlTabReady(false);
118
+ const timer = setTimeout(() => {
119
+ setIsHtmlTabReady(true);
120
+ }, 120);
121
+ return () => clearTimeout(timer);
122
+ }, [htmlSubTab, webViewSubTab, activeTab]);
107
123
  const [selectedEvent, setSelectedEvent] = useState(null);
108
124
  const [analyticsSearch, setAnalyticsSearch] = useState('');
109
125
  const [hideScreenView, setHideScreenView] = useState(true);
110
126
  const [analyticsSubTab, setAnalyticsSubTab] = useState('ga_events');
111
- const [groupByScreen, setGroupByScreen] = useState(false);
112
- const [expandedScreens, setExpandedScreens] = useState(new Set());
113
127
  const [topEventsExpanded, setTopEventsExpanded] = useState(true);
114
128
  const [newEventIds, setNewEventIds] = useState(new Set());
115
129
  const prevEventIdsRef = useRef(new Set());
@@ -249,6 +263,10 @@ const NetworkInspector = () => {
249
263
  setWebViewHtmlUrl(getWebViewHtmlUrl());
250
264
  }, 200);
251
265
  });
266
+ setReduxState(getReduxState());
267
+ const unsubscribeRedux = subscribeReduxState(() => {
268
+ setReduxState(getReduxState());
269
+ });
252
270
  return () => {
253
271
  unsubscribe();
254
272
  clearTimeout(timeoutId);
@@ -258,6 +276,7 @@ const NetworkInspector = () => {
258
276
  clearTimeout(consoleTimeoutId);
259
277
  unsubscribeWebView();
260
278
  clearTimeout(webViewTimeoutId);
279
+ unsubscribeRedux();
261
280
  };
262
281
  }, []);
263
282
  useEffect(() => {
@@ -542,39 +561,6 @@ const NetworkInspector = () => {
542
561
  analytics: `${searchedLogs.filter(l => l.message.toLowerCase().includes('[analytics error]')).length}/${total}`,
543
562
  };
544
563
  }, [visibleConsoleLogs, logSearch]);
545
- const groupedAnalyticsEvents = useMemo(() => {
546
- if (!groupByScreen)
547
- return [];
548
- const map = new Map();
549
- for (const e of filteredAnalyticsEvents) {
550
- const routeInfo = logRouteMapRef.current.get(e.id + 1000000);
551
- let screenName = e.screenName ||
552
- e.screenClass ||
553
- e.pageTitle ||
554
- e.pageLocation ||
555
- e.params?.firebase_screen ||
556
- e.params?.screen_name ||
557
- e.params?.firebase_screen_class ||
558
- e.params?.screen_class;
559
- if (!screenName) {
560
- if (routeInfo && routeInfo.path !== 'Navigators') {
561
- const parts = routeInfo.path.split(' ➔ ');
562
- screenName = parts[parts.length - 1];
563
- }
564
- else {
565
- screenName = 'Unknown Component';
566
- }
567
- }
568
- if (!map.has(screenName))
569
- map.set(screenName, []);
570
- map.get(screenName).push(e);
571
- }
572
- const sections = [];
573
- for (const [title, data] of map.entries()) {
574
- sections.push({ title, data });
575
- }
576
- return sections;
577
- }, [groupByScreen, filteredAnalyticsEvents]);
578
564
  const topEventsArray = useMemo(() => {
579
565
  const freq = {};
580
566
  filteredAnalyticsEvents.forEach(e => {
@@ -584,51 +570,6 @@ const NetworkInspector = () => {
584
570
  });
585
571
  return Object.entries(freq).sort((a, b) => b[1] - a[1]);
586
572
  }, [filteredAnalyticsEvents]);
587
- const groupedNetworkLogs = useMemo(() => {
588
- if (!groupByScreen)
589
- return [];
590
- const map = new Map();
591
- for (const l of filteredLogs) {
592
- const routeInfo = logRouteMapRef.current.get(l.id);
593
- let screenName = 'Unknown Origin';
594
- if (routeInfo && routeInfo.path !== 'Navigators') {
595
- const parts = routeInfo.path.split(' ➔ ');
596
- screenName = parts[parts.length - 1];
597
- }
598
- if (!map.has(screenName))
599
- map.set(screenName, []);
600
- map.get(screenName).push(l);
601
- }
602
- const sections = [];
603
- for (const [title, data] of map.entries()) {
604
- sections.push({ title, data });
605
- }
606
- return sections;
607
- }, [groupByScreen, filteredLogs]);
608
- const toggleScreenAccordion = useCallback((title) => {
609
- setExpandedScreens(prev => {
610
- const next = new Set(prev);
611
- if (next.has(title))
612
- next.delete(title);
613
- else
614
- next.add(title);
615
- return next;
616
- });
617
- }, []);
618
- const renderScreenSectionHeader = useCallback(({ section: { title, data } }) => {
619
- const isExpanded = expandedScreens.has(title);
620
- return (<Pressable onPress={() => toggleScreenAccordion(title)} style={styles.screenSectionHeader}>
621
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
622
- <View style={{
623
- transform: [{ rotate: isExpanded ? '180deg' : '0deg' }],
624
- }}>
625
- <ExpandCollapseIcon color={AppColors.primaryBlack} size={14} isExpanded={false}/>
626
- </View>
627
- <Text style={styles.screenSectionTitle}>{title}</Text>
628
- </View>
629
- <Text style={styles.screenSectionCount}>{data.length} logs</Text>
630
- </Pressable>);
631
- }, [expandedScreens, toggleScreenAccordion]);
632
573
  function closeModal() {
633
574
  setVisible(false);
634
575
  setSelected(null);
@@ -775,12 +716,317 @@ const NetworkInspector = () => {
775
716
  toggleSectionFilter,
776
717
  toggleSectionCollapse,
777
718
  ]);
719
+ const renderInsightsDashboard = () => {
720
+ const apiTotal = logs.length;
721
+ const apiErrors = logs.filter(l => (l.status != null && l.status >= 400) || l.status === 0 || l.status == null).length;
722
+ const apiSuccess = apiTotal - apiErrors;
723
+ const apiSuccessRate = apiTotal > 0 ? Math.round((apiSuccess / apiTotal) * 100) : 100;
724
+ const durations = logs.filter(l => l.duration != null).map(l => l.duration);
725
+ const avgTime = durations.length > 0
726
+ ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
727
+ : null;
728
+ const logTotal = visibleConsoleLogs.length;
729
+ const logErrors = visibleConsoleLogs.filter(l => l.type === 'error').length;
730
+ const logWarns = visibleConsoleLogs.filter(l => l.type === 'warn').length;
731
+ const logInfos = visibleConsoleLogs.filter(l => l.type === 'info').length;
732
+ const analyticsTotal = analyticsEvents.length;
733
+ const uniqueEvents = new Set(analyticsEvents.map(e => e.name)).size;
734
+ const screenViews = analyticsEvents.filter(e => e.name === 'screen_view' || e.name === 'page_view' || e.name === 'firebase_screen_class').length;
735
+ const webviewTotal = webViewNavHistory.length;
736
+ return (<View style={styles.dashboardContainer}>
737
+ {/* Module 1: APIs */}
738
+ <TouchableScale style={styles.dashboardModuleCard} onPress={() => setActiveTab('apis')}>
739
+ <View style={styles.dashboardModuleHeader}>
740
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
741
+ <SignalIcon color={AppColors.purple} size={18}/>
742
+ <Text style={styles.dashboardModuleTitle}>APIs & Network</Text>
743
+ </View>
744
+ <Text style={styles.dashboardModuleGoText}>View Details →</Text>
745
+ </View>
746
+ <View style={styles.dashboardModuleGrid}>
747
+ <View style={styles.dashboardGridItem}>
748
+ <Text style={styles.dashboardGridVal}>{apiTotal}</Text>
749
+ <Text style={styles.dashboardGridLbl}>Requests</Text>
750
+ </View>
751
+ <View style={styles.dashboardGridItem}>
752
+ <Text style={[styles.dashboardGridVal, apiSuccessRate < 90 && { color: AppColors.warningIconGold }]}>
753
+ {apiSuccessRate}%
754
+ </Text>
755
+ <Text style={styles.dashboardGridLbl}>Success Rate</Text>
756
+ </View>
757
+ <View style={styles.dashboardGridItem}>
758
+ <Text style={[styles.dashboardGridVal, apiErrors > 0 && { color: AppColors.errorColor }]}>
759
+ {apiErrors}
760
+ </Text>
761
+ <Text style={styles.dashboardGridLbl}>Errors</Text>
762
+ </View>
763
+ <View style={styles.dashboardGridItem}>
764
+ <Text style={styles.dashboardGridVal}>
765
+ {avgTime != null ? `${avgTime}ms` : '—'}
766
+ </Text>
767
+ <Text style={styles.dashboardGridLbl}>Avg Latency</Text>
768
+ </View>
769
+ </View>
770
+ </TouchableScale>
771
+
772
+ {/* Module 2: Logs */}
773
+ <TouchableScale style={styles.dashboardModuleCard} onPress={() => setActiveTab('logs')}>
774
+ <View style={styles.dashboardModuleHeader}>
775
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
776
+ <TerminalIcon color="#0D9488" size={18}/>
777
+ <Text style={styles.dashboardModuleTitle}>Console Logs</Text>
778
+ </View>
779
+ <Text style={styles.dashboardModuleGoText}>View Details →</Text>
780
+ </View>
781
+ <View style={styles.dashboardModuleGrid}>
782
+ <View style={styles.dashboardGridItem}>
783
+ <Text style={styles.dashboardGridVal}>{logTotal}</Text>
784
+ <Text style={styles.dashboardGridLbl}>Total Logs</Text>
785
+ </View>
786
+ <View style={styles.dashboardGridItem}>
787
+ <Text style={[styles.dashboardGridVal, { color: '#0D9488' }]}>{logInfos}</Text>
788
+ <Text style={styles.dashboardGridLbl}>Info</Text>
789
+ </View>
790
+ <View style={styles.dashboardGridItem}>
791
+ <Text style={[styles.dashboardGridVal, logWarns > 0 && { color: AppColors.warningIconGold }]}>
792
+ {logWarns}
793
+ </Text>
794
+ <Text style={styles.dashboardGridLbl}>Warnings</Text>
795
+ </View>
796
+ <View style={styles.dashboardGridItem}>
797
+ <Text style={[styles.dashboardGridVal, logErrors > 0 && { color: AppColors.errorColor }]}>
798
+ {logErrors}
799
+ </Text>
800
+ <Text style={styles.dashboardGridLbl}>Errors</Text>
801
+ </View>
802
+ </View>
803
+ </TouchableScale>
804
+
805
+ {/* Module 3: Analytics */}
806
+ <TouchableScale style={styles.dashboardModuleCard} onPress={() => setActiveTab('analytics')}>
807
+ <View style={styles.dashboardModuleHeader}>
808
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
809
+ <AnalyticsIcon color="#EA580C" size={18}/>
810
+ <Text style={styles.dashboardModuleTitle}>Analytics Events</Text>
811
+ </View>
812
+ <Text style={styles.dashboardModuleGoText}>View Details →</Text>
813
+ </View>
814
+ <View style={styles.dashboardModuleGrid}>
815
+ <View style={styles.dashboardGridItem}>
816
+ <Text style={styles.dashboardGridVal}>{analyticsTotal}</Text>
817
+ <Text style={styles.dashboardGridLbl}>Total Events</Text>
818
+ </View>
819
+ <View style={styles.dashboardGridItem}>
820
+ <Text style={[styles.dashboardGridVal, { color: '#EA580C' }]}>{uniqueEvents}</Text>
821
+ <Text style={styles.dashboardGridLbl}>Unique Names</Text>
822
+ </View>
823
+ <View style={styles.dashboardGridItem}>
824
+ <Text style={styles.dashboardGridVal}>{screenViews}</Text>
825
+ <Text style={styles.dashboardGridLbl}>Screen Views</Text>
826
+ </View>
827
+ <View style={styles.dashboardGridItem}>
828
+ <Text style={styles.dashboardGridVal}>
829
+ {analyticsTotal > 0 ? Math.round(analyticsTotal / Math.max(1, logs.length / 5)) : 0}
830
+ </Text>
831
+ <Text style={styles.dashboardGridLbl}>Events Ratio</Text>
832
+ </View>
833
+ </View>
834
+ </TouchableScale>
835
+
836
+ {/* Module 4: WebView */}
837
+ <TouchableScale style={styles.dashboardModuleCard} onPress={() => setActiveTab('webview')}>
838
+ <View style={styles.dashboardModuleHeader}>
839
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
840
+ <GlobeIcon color="#2563EB" size={18}/>
841
+ <Text style={styles.dashboardModuleTitle}>WebView Captures</Text>
842
+ </View>
843
+ <Text style={styles.dashboardModuleGoText}>View Details →</Text>
844
+ </View>
845
+ <View style={styles.dashboardModuleGrid}>
846
+ <View style={styles.dashboardGridItem}>
847
+ <Text style={styles.dashboardGridVal}>{webviewTotal}</Text>
848
+ <Text style={styles.dashboardGridLbl}>History Size</Text>
849
+ </View>
850
+ <View style={styles.dashboardGridItem}>
851
+ <Text style={[styles.dashboardGridVal, { color: '#16A34A' }]}>Active</Text>
852
+ <Text style={styles.dashboardGridLbl}>Status</Text>
853
+ </View>
854
+ <View style={styles.dashboardGridItem}>
855
+ <Text numberOfLines={1} style={styles.dashboardGridVal}>
856
+ {webviewTotal > 0 ? `${webViewNavHistory[0]?.title?.substring(0, 10) ?? ''}...` : '—'}
857
+ </Text>
858
+ <Text style={styles.dashboardGridLbl}>Last URL</Text>
859
+ </View>
860
+ </View>
861
+ </TouchableScale>
862
+
863
+ {/* Module 5: Redux Store */}
864
+ <TouchableScale style={styles.dashboardModuleCard} onPress={() => setActiveTab('redux')}>
865
+ <View style={styles.dashboardModuleHeader}>
866
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
867
+ <TerminalIcon color={AppColors.purple} size={18}/>
868
+ <Text style={styles.dashboardModuleTitle}>Redux Store State</Text>
869
+ </View>
870
+ <Text style={styles.dashboardModuleGoText}>View Details →</Text>
871
+ </View>
872
+ {reduxState ? (<View style={{ paddingHorizontal: 12, paddingBottom: 12, gap: 6 }}>
873
+ <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 }}>
874
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 10, color: AppColors.grayTextWeak, letterSpacing: 0.5 }}>
875
+ REDUCER NAME
876
+ </Text>
877
+ <Text style={{ fontFamily: AppFonts.interBold, fontSize: 10, color: AppColors.grayTextWeak, letterSpacing: 0.5 }}>
878
+ SIZE / FIELDS
879
+ </Text>
880
+ </View>
881
+ {Object.keys(reduxState).map(key => {
882
+ const val = reduxState[key];
883
+ const fieldsCount = typeof val === 'object' && val !== null ? Object.keys(val).length : 0;
884
+ const sizeStr = getSize(val);
885
+ return (<View key={key} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 2 }}>
886
+ <Text style={{ fontFamily: AppFonts.interMedium, fontSize: 12, color: AppColors.grayTextStrong }}>
887
+ {key}
888
+ </Text>
889
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 11, color: AppColors.grayTextWeak }}>
890
+ {sizeStr} ({fieldsCount} fields)
891
+ </Text>
892
+ </View>);
893
+ })}
894
+ </View>) : (<View style={{ padding: 12, alignItems: 'center' }}>
895
+ <Text style={{ fontFamily: AppFonts.interRegular, fontSize: 12, color: AppColors.grayTextWeak }}>
896
+ No connected Redux store.
897
+ </Text>
898
+ </View>)}
899
+ </TouchableScale>
900
+ </View>);
901
+ };
902
+ const renderReduxTab = () => {
903
+ if (!reduxState) {
904
+ return (<View style={styles.emptyContainer}>
905
+ <View style={styles.emptyIconWrap}>
906
+ <TerminalIcon color={AppColors.purple} size={32}/>
907
+ </View>
908
+ <Text style={styles.emptyTitle}>No Redux Store</Text>
909
+ <Text style={styles.emptySub}>
910
+ To inspect Redux store, call connectReduxStore(store) at app start.
911
+ </Text>
912
+ </View>);
913
+ }
914
+ const reducerKeys = Object.keys(reduxState);
915
+ if (reducerKeys.length === 0) {
916
+ return (<View style={styles.emptyContainer}>
917
+ <Text style={styles.emptyTitle}>Empty Store</Text>
918
+ <Text style={styles.emptySub}>Connected store state is empty.</Text>
919
+ </View>);
920
+ }
921
+ return (<ScrollView style={styles.detailScroll} contentContainerStyle={{ paddingBottom: 24 }}>
922
+ <View style={{
923
+ flexDirection: 'row',
924
+ alignItems: 'center',
925
+ justifyContent: 'space-between',
926
+ paddingHorizontal: 16,
927
+ paddingVertical: 12,
928
+ borderBottomWidth: 1,
929
+ borderBottomColor: AppColors.dividerColor,
930
+ backgroundColor: AppColors.primaryLight,
931
+ }}>
932
+ <Text style={{
933
+ fontFamily: AppFonts.interBold,
934
+ color: AppColors.grayTextStrong,
935
+ fontSize: 12,
936
+ textTransform: 'uppercase',
937
+ letterSpacing: 0.6,
938
+ }}>
939
+ Redux Store ({reducerKeys.length} Reducers)
940
+ </Text>
941
+ <CopyButton value={() => reduxState} label="Overall Store"/>
942
+ </View>
943
+
944
+ <View style={{
945
+ flexDirection: 'row',
946
+ alignItems: 'center',
947
+ backgroundColor: AppColors.grayBackground,
948
+ borderRadius: 8,
949
+ marginHorizontal: 16,
950
+ marginTop: 12,
951
+ marginBottom: 8,
952
+ paddingHorizontal: 10,
953
+ borderWidth: 1,
954
+ borderColor: AppColors.dividerColor,
955
+ height: 36,
956
+ }}>
957
+ <TextInput placeholder="Search Redux keys or values..." placeholderTextColor={AppColors.grayTextWeak} value={reduxSearch} onChangeText={setReduxSearch} style={{
958
+ flex: 1,
959
+ fontFamily: AppFonts.interRegular,
960
+ fontSize: 12,
961
+ color: AppColors.grayTextStrong,
962
+ padding: 0,
963
+ }} autoCorrect={false} autoCapitalize="none"/>
964
+ {reduxSearch.length > 0 && (<Pressable onPress={() => setReduxSearch('')} hitSlop={10}>
965
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
966
+ </Pressable>)}
967
+ </View>
968
+
969
+ {reducerKeys.map(key => {
970
+ const isExpanded = expandedReducers[key];
971
+ const val = reduxState[key];
972
+ return (<View key={key} style={{
973
+ backgroundColor: AppColors.primaryLight,
974
+ borderBottomWidth: 1,
975
+ borderBottomColor: AppColors.dividerColor,
976
+ }}>
977
+ <Pressable onPress={() => {
978
+ setExpandedReducers(prev => ({
979
+ ...prev,
980
+ [key]: !prev[key],
981
+ }));
982
+ }} style={{
983
+ flexDirection: 'row',
984
+ alignItems: 'center',
985
+ justifyContent: 'space-between',
986
+ paddingHorizontal: 16,
987
+ paddingVertical: 12,
988
+ }}>
989
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 }}>
990
+ <Animated.View style={{ transform: [{ rotate: isExpanded ? '0deg' : '-90deg' }] }}>
991
+ <ChevronIcon color={AppColors.grayTextWeak} size={14}/>
992
+ </Animated.View>
993
+ <Text style={{
994
+ fontFamily: AppFonts.interBold,
995
+ fontSize: 13,
996
+ color: AppColors.purple,
997
+ }}>
998
+ {key}
999
+ </Text>
1000
+ <Text style={{
1001
+ fontFamily: AppFonts.interRegular,
1002
+ fontSize: 11,
1003
+ color: AppColors.grayTextWeak,
1004
+ }}>
1005
+ ({typeof val === 'object' && val !== null ? `${Object.keys(val).length} fields` : typeof val})
1006
+ </Text>
1007
+ </View>
1008
+ <CopyButton value={() => val} label={`${key} Reducer`}/>
1009
+ </Pressable>
1010
+
1011
+ {isExpanded && (<View style={{
1012
+ backgroundColor: AppColors.grayBackground,
1013
+ paddingHorizontal: 12,
1014
+ paddingVertical: 8,
1015
+ borderTopWidth: 1,
1016
+ borderTopColor: AppColors.dividerColor,
1017
+ }}>
1018
+ <JsonViewer data={val} search={reduxSearch}/>
1019
+ </View>)}
1020
+ </View>);
1021
+ })}
1022
+ </ScrollView>);
1023
+ };
778
1024
  return (<>
779
1025
  {hasNavigationContext && (<NavigationTracker onStateChange={setNavState}/>)}
780
1026
  <TouchableScale style={styles.fabWrapper} onPress={() => setVisible(true)} hitSlop={10}>
781
1027
  <Animated.View style={[styles.fabPulseRing, { transform: [{ scale: pulseAnim }] }]}/>
782
1028
  <LinearGradient colors={[AppColors.purple, '#8F6EFF']} style={styles.fab}>
783
- <Text style={styles.fabText}>API</Text>
1029
+ <DebugIcon color="#FFFFFF" size={28}/>
784
1030
  </LinearGradient>
785
1031
  {(logs.length > 0 || analyticsEvents.length > 0) && (<Animated.View style={[
786
1032
  styles.fabBadge,
@@ -796,23 +1042,24 @@ const NetworkInspector = () => {
796
1042
  </TouchableScale>
797
1043
 
798
1044
  <Modal visible={visible} animationType="slide" transparent>
799
- {visible && (<View style={{ flex: 1, backgroundColor: AppColors.grayBackground }}>
800
- <StatusBar translucent backgroundColor="transparent" barStyle="light-content"/>
1045
+ {visible && (<ErrorBoundary onClose={closeModal}>
1046
+ <View style={styles.modalBackdrop}>
1047
+ <Pressable style={styles.modalBackdropPressable} onPress={closeModal}/>
1048
+ <View style={styles.modalContentCard}>
1049
+ <StatusBar translucent backgroundColor="transparent" barStyle="light-content"/>
801
1050
 
802
- <LinearGradient colors={[AppColors.purple, '#6B4EFF']} style={styles.headerGradient}>
803
- <View style={[
804
- styles.header,
1051
+ <LinearGradient colors={[AppColors.purple, '#6B4EFF']} style={styles.headerGradient}>
1052
+ <View style={styles.header}>
1053
+ <View style={[
1054
+ styles.headerLeft,
805
1055
  {
806
- paddingTop: Platform.OS === 'android'
807
- ? (StatusBar.currentHeight ?? 24) + 16
808
- : 54,
1056
+ flexDirection: 'row',
1057
+ alignItems: 'center',
1058
+ gap: 16,
1059
+ flex: (selected == null && selectedEvent == null) ? 5 : 1,
809
1060
  },
810
1061
  ]}>
811
- <View style={[
812
- styles.headerLeft,
813
- { flexDirection: 'row', alignItems: 'center', gap: 16 },
814
- ]}>
815
- <TouchableScale onPress={() => {
1062
+ <TouchableScale onPress={() => {
816
1063
  requestAnimationFrame(() => {
817
1064
  setSelected(null);
818
1065
  setSelectedEvent(null);
@@ -822,100 +1069,48 @@ const NetworkInspector = () => {
822
1069
  selected == null &&
823
1070
  selectedEvent == null && { display: 'none' },
824
1071
  ]}>
825
- <WhiteBackNavigation />
826
- </TouchableScale>
827
-
828
- {selected == null && selectedEvent == null ? (<View style={styles.headerButtonGroup}>
829
- {/* Network Dropdown */}
830
- <Pressable onPress={() => {
831
- setShowNetworkMenu(prev => !prev);
832
- setShowUiMenu(false);
833
- }} style={[
834
- styles.headerGroupButton,
835
- ['apis', 'analytics', 'logs'].includes(activeTab) &&
836
- styles.headerGroupButtonActive,
837
- ]}>
838
- <Text style={[
839
- styles.headerGroupButtonText,
840
- ['apis', 'analytics', 'logs'].includes(activeTab) && { color: '#FFFFFF' },
841
- ]}>
842
- APIs
843
- </Text>
844
- <View style={{
845
- transform: [
846
- { rotate: showNetworkMenu ? '180deg' : '0deg' },
847
- ],
848
- }}>
849
- <ChevronIcon color={['apis', 'analytics', 'logs'].includes(activeTab)
850
- ? '#FFFFFF'
851
- : 'rgba(255, 255, 255, 0.6)'} size={10}/>
852
- </View>
853
- </Pressable>
1072
+ <WhiteBackNavigation />
1073
+ </TouchableScale>
854
1074
 
855
- {/* UI Dropdown */}
856
- <Pressable onPress={() => {
857
- setShowUiMenu(prev => !prev);
858
- setShowNetworkMenu(false);
859
- }} style={[
860
- styles.headerGroupButton,
861
- activeTab === 'webview' &&
862
- styles.headerGroupButtonActive,
863
- ]}>
864
- <Text style={[
865
- styles.headerGroupButtonText,
866
- activeTab === 'webview' && { color: '#FFFFFF' },
867
- ]}>
868
- UI
869
- </Text>
870
- <View style={{
871
- transform: [
872
- { rotate: showUiMenu ? '180deg' : '0deg' },
873
- ],
874
- }}>
875
- <ChevronIcon color={activeTab === 'webview'
876
- ? '#FFFFFF'
877
- : 'rgba(255, 255, 255, 0.6)'} size={10}/>
878
- </View>
879
- </Pressable>
880
- </View>) : null}
881
- </View>
1075
+ {selected == null && selectedEvent == null ? (<Text style={styles.headerTitle}>RN-InApp-Inspector</Text>) : null}
1076
+ </View>
882
1077
 
883
- <View style={styles.headerCenter}>
884
- {selected != null ? (<View style={styles.headerDetailCenter}>
885
- <View style={styles.headerDetailRow}>
886
- <View style={[
1078
+ <View style={styles.headerCenter}>
1079
+ {selected != null ? (<View style={styles.headerDetailCenter}>
1080
+ <View style={styles.headerDetailRow}>
1081
+ <View style={[
887
1082
  styles.headerMethodBadge,
888
1083
  {
889
1084
  backgroundColor: METHOD_COLORS[selected.method] ??
890
1085
  AppColors.grayText,
891
1086
  },
892
1087
  ]}>
893
- <Text style={styles.headerMethodText}>
894
- {selected.method}
1088
+ <Text style={styles.headerMethodText}>
1089
+ {selected.method}
1090
+ </Text>
1091
+ </View>
1092
+ <Text style={styles.headerDetailTitle} numberOfLines={1} ellipsizeMode="middle">
1093
+ {detailTitle}
895
1094
  </Text>
896
1095
  </View>
897
- <Text style={styles.headerDetailTitle} numberOfLines={1} ellipsizeMode="middle">
898
- {detailTitle}
899
- </Text>
900
- </View>
901
- <View style={styles.headerDetailSubRow}>
902
- <View style={[
1096
+ <View style={styles.headerDetailSubRow}>
1097
+ <View style={[
903
1098
  styles.headerStatusDot,
904
1099
  { backgroundColor: getStatusColor(selected.status) },
905
1100
  ]}/>
906
- <Text style={styles.headerSubTitle}>
907
- {selected.status === 0
1101
+ <Text style={styles.headerSubTitle}>
1102
+ {selected.status === 0
908
1103
  ? 'Failed'
909
1104
  : selected.status ?? 'Pending'}{' '}
910
- •{' '}
911
- {selected.duration != null
1105
+ •{' '}
1106
+ {selected.duration != null
912
1107
  ? `${selected.duration}ms`
913
1108
  : '-'}
914
- </Text>
915
- </View>
916
- </View>) : selectedEvent != null ? (<View style={styles.headerDetailCenter}>
917
- <View style={styles.headerDetailRow}>
918
- <View style={[
1109
+ </Text>
1110
+ </View>
1111
+ </View>) : selectedEvent != null ? (<View style={styles.headerDetailCenter}>
1112
+ <View style={styles.headerDetailRow}>
1113
+ <View style={[
919
1114
  styles.headerMethodBadge,
920
1115
  {
921
1116
  backgroundColor: selectedEvent.source === 'firebase'
@@ -923,16 +1118,16 @@ const NetworkInspector = () => {
923
1118
  : 'rgba(124,92,191,0.3)',
924
1119
  },
925
1120
  ]}>
926
- <Text style={styles.headerMethodText}>
927
- {selectedEvent.source === 'firebase' ? 'FB' : 'MAN'}
1121
+ <Text style={styles.headerMethodText}>
1122
+ {selectedEvent.source === 'firebase' ? 'FB' : 'MAN'}
1123
+ </Text>
1124
+ </View>
1125
+ <Text style={styles.headerDetailTitle} numberOfLines={1} ellipsizeMode="middle">
1126
+ {selectedEvent.name}
928
1127
  </Text>
929
1128
  </View>
930
- <Text style={styles.headerDetailTitle} numberOfLines={1} ellipsizeMode="middle">
931
- {selectedEvent.name}
932
- </Text>
933
- </View>
934
- <View style={styles.headerDetailSubRow}>
935
- <View style={[
1129
+ <View style={styles.headerDetailSubRow}>
1130
+ <View style={[
936
1131
  styles.headerStatusDot,
937
1132
  {
938
1133
  backgroundColor: selectedEvent.source === 'firebase'
@@ -940,133 +1135,76 @@ const NetworkInspector = () => {
940
1135
  : AppColors.purple,
941
1136
  },
942
1137
  ]}/>
943
- <Text style={styles.headerSubTitle}>
944
- {Object.keys(selectedEvent.params).length} param
945
- {Object.keys(selectedEvent.params).length !== 1
1138
+ <Text style={styles.headerSubTitle}>
1139
+ {Object.keys(selectedEvent.params).length} param
1140
+ {Object.keys(selectedEvent.params).length !== 1
946
1141
  ? 's'
947
1142
  : ''}
948
- {' · '}
949
- {selectedEvent.source}
950
- </Text>
951
- </View>
952
- </View>) : (<View style={styles.headerCountBadge}>
953
- <Text style={styles.headerCountText}>
954
- {activeTab === 'apis'
955
- ? `${logs.length} API${logs.length !== 1 ? 's' : ''}`
956
- : activeTab === 'logs'
957
- ? `${visibleConsoleLogs.length} log${visibleConsoleLogs.length !== 1 ? 's' : ''}`
958
- : activeTab === 'analytics'
959
- ? `${analyticsEvents.length} event${analyticsEvents.length !== 1 ? 's' : ''}`
960
- : `${webViewLogs.length} log${webViewLogs.length !== 1 ? 's' : ''}`}
961
- </Text>
962
- </View>)}
963
- </View>
964
-
965
- <View style={styles.headerRight}>
966
- <TouchableScale onPress={closeModal} hitSlop={15} style={styles.iconBtnMinimal}>
967
- <CloseWhite />
968
- </TouchableScale>
969
- </View>
970
- </View>
971
- </LinearGradient>
1143
+ {' · '}
1144
+ {selectedEvent.source}
1145
+ </Text>
1146
+ </View>
1147
+ </View>) : null}
1148
+ </View>
972
1149
 
973
- {(showNetworkMenu || showUiMenu) && selected == null && (<Pressable style={{
974
- position: 'absolute',
975
- top: 0,
976
- left: 0,
977
- right: 0,
978
- bottom: 0,
979
- zIndex: 99,
980
- }} onPress={() => {
981
- setShowNetworkMenu(false);
982
- setShowUiMenu(false);
983
- }}/>)}
1150
+ <View style={styles.headerRight}>
1151
+ <TouchableScale onPress={() => {
1152
+ const newTheme = !isDark;
1153
+ setIsDark(newTheme);
1154
+ toggleGlobalTheme(newTheme);
1155
+ }} hitSlop={15} style={[styles.closeButtonCircle, { marginRight: 8 }]}>
1156
+ {isDark ? (<SunIcon color="#FFFFFF" size={16}/>) : (<MoonIcon color="#FFFFFF" size={16}/>)}
1157
+ </TouchableScale>
984
1158
 
985
- {selected == null && selectedEvent == null && (<View style={{
986
- paddingHorizontal: 16,
987
- paddingTop: 10,
988
- paddingBottom: 2,
989
- }}>
990
- <Text style={{
991
- fontFamily: AppFonts.interMedium,
992
- fontSize: 11.5,
993
- color: AppColors.grayTextWeak,
994
- }}>
995
- {['apis', 'analytics', 'logs'].includes(activeTab)
996
- ? 'APIs'
997
- : 'UI'}
998
- {' ➔ '}
999
- <Text style={{
1000
- color: AppColors.purple,
1001
- fontFamily: AppFonts.interBold,
1002
- }}>
1003
- {activeTab === 'apis'
1004
- ? 'APIs'
1005
- : activeTab === 'analytics'
1006
- ? 'Analytics'
1007
- : activeTab === 'logs'
1008
- ? 'Logs'
1009
- : 'WebView'}
1010
- </Text>
1011
- </Text>
1012
- </View>)}
1159
+ <TouchableScale onPress={closeModal} hitSlop={15} style={styles.closeButtonCircle}>
1160
+ <CloseWhite size={16}/>
1161
+ </TouchableScale>
1162
+ </View>
1163
+ </View>
1164
+ </LinearGradient>
1013
1165
 
1014
- {showNetworkMenu && selected == null && selectedEvent == null && (<View style={[styles.menuDropdown, { left: 16 }]}>
1015
- {['apis', 'analytics', 'logs'].map(tab => {
1016
- const label = tab === 'apis'
1017
- ? 'APIs'
1018
- : tab === 'analytics'
1019
- ? 'Analytics'
1020
- : 'Logs';
1021
- const count = tab === 'apis'
1022
- ? logs.length
1023
- : tab === 'analytics'
1024
- ? analyticsEvents.length
1025
- : visibleConsoleLogs.length;
1026
- const isActive = activeTab === tab;
1027
- return (<Pressable key={tab} style={[
1028
- styles.dropdownItem,
1029
- isActive && { backgroundColor: `${AppColors.purple}12` },
1030
- ]} onPress={() => {
1031
- setActiveTab(tab);
1032
- setShowNetworkMenu(false);
1033
- }} hitSlop={10}>
1034
- <Text style={{
1035
- fontFamily: AppFonts.interMedium,
1036
- color: isActive
1037
- ? AppColors.purple
1038
- : AppColors.primaryBlack,
1039
- }}>
1040
- {label} {count > 0 ? `(${count})` : ''}
1041
- </Text>
1042
- </Pressable>);
1166
+ {/* ─── Horizontal Scrollable Tab Bar inside Content ─── */}
1167
+ {selected == null && selectedEvent == null ? (<View style={styles.tabBarContainer}>
1168
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingRight: 16 }}>
1169
+ {[
1170
+ { key: 'insights', label: 'Insights', count: 0, icon: 'insights' },
1171
+ { key: 'apis', label: 'APIs', count: logs.length, icon: 'apis' },
1172
+ { key: 'logs', label: 'Logs', count: consoleLogs.length, icon: 'logs' },
1173
+ { key: 'analytics', label: 'Analytics', count: analyticsEvents.length, icon: 'analytics' },
1174
+ { key: 'webview', label: 'WebView', count: webViewNavHistory.length, icon: 'webview' },
1175
+ { key: 'redux', label: 'Redux', count: 0, icon: 'redux' },
1176
+ ].map(tab => {
1177
+ const isActive = activeTab === tab.key;
1178
+ const iconColor = isActive ? '#FFFFFF' : AppColors.grayText;
1179
+ const countLabel = tab.count > 9 ? '9+' : String(tab.count);
1180
+ return (<TouchableScale key={tab.key} onPress={() => {
1181
+ requestAnimationFrame(() => {
1182
+ setActiveTab(tab.key);
1183
+ });
1184
+ }} style={[
1185
+ styles.contentTabButton,
1186
+ isActive && styles.contentTabButtonActive,
1187
+ ]}>
1188
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
1189
+ {tab.icon === 'insights' && <InsightsIcon color={iconColor} size={14}/>}
1190
+ {tab.icon === 'apis' && <SignalIcon color={iconColor} size={14}/>}
1191
+ {tab.icon === 'logs' && <TerminalIcon color={iconColor} size={14}/>}
1192
+ {tab.icon === 'analytics' && <AnalyticsIcon color={iconColor} size={14}/>}
1193
+ {tab.icon === 'webview' && <GlobeIcon color={iconColor} size={14}/>}
1194
+ {tab.icon === 'redux' && <TerminalIcon color={iconColor} size={14}/>}
1195
+ <Text numberOfLines={1} ellipsizeMode="tail" style={[
1196
+ styles.contentTabButtonText,
1197
+ isActive && styles.contentTabButtonTextActive,
1198
+ ]}>
1199
+ {tab.label} {tab.count > 0 ? `(${countLabel})` : ''}
1200
+ </Text>
1201
+ </View>
1202
+ </TouchableScale>);
1043
1203
  })}
1044
- </View>)}
1204
+ </ScrollView>
1205
+ </View>) : null}
1206
+
1045
1207
 
1046
- {showUiMenu && selected == null && selectedEvent == null && (<View style={[styles.menuDropdown, { left: 120 }]}>
1047
- {['webview'].map(tab => {
1048
- const isActive = activeTab === tab;
1049
- return (<Pressable key={tab} style={[
1050
- styles.dropdownItem,
1051
- isActive && { backgroundColor: `${AppColors.purple}12` },
1052
- ]} onPress={() => {
1053
- setActiveTab(tab);
1054
- setShowUiMenu(false);
1055
- }} hitSlop={10}>
1056
- <Text style={{
1057
- fontFamily: AppFonts.interMedium,
1058
- color: isActive
1059
- ? AppColors.purple
1060
- : AppColors.primaryBlack,
1061
- }}>
1062
- WebView{' '}
1063
- {webViewLogs.length > 0
1064
- ? `(${webViewLogs.length})`
1065
- : ''}
1066
- </Text>
1067
- </Pressable>);
1068
- })}
1069
- </View>)}
1070
1208
 
1071
1209
  {/* ─── Secondary Tab Bar for Analytics ──────────────────────── */}
1072
1210
  {isReady && activeTab === 'analytics' && selectedEvent == null && (<View>
@@ -1080,14 +1218,6 @@ const NetworkInspector = () => {
1080
1218
  </Pressable>)}
1081
1219
  </View>
1082
1220
  {analyticsSubTab === 'ga_events' && (<View style={styles.toolbarRight}>
1083
- <TouchableScale style={[
1084
- styles.toolbarBtn,
1085
- groupByScreen && styles.toolbarBtnActive,
1086
- ]} onPress={() => setGroupByScreen(prev => !prev)} hitSlop={10}>
1087
- <MapPinIcon color={groupByScreen
1088
- ? AppColors.purple
1089
- : AppColors.grayTextStrong} size={18}/>
1090
- </TouchableScale>
1091
1221
  <TouchableScale style={[
1092
1222
  styles.toolbarBtn,
1093
1223
  !hideScreenView && styles.toolbarBtnActive,
@@ -1181,17 +1311,19 @@ const NetworkInspector = () => {
1181
1311
  </View>
1182
1312
  </View>)}
1183
1313
 
1184
- {isReady ? (activeTab === 'analytics' ? (selectedEvent != null ? (<AnalyticsDetail event={selectedEvent}/>) : analyticsSubTab === 'top_events' ? (<FlatList data={topEventsArray} keyExtractor={item => item[0]} contentContainerStyle={[
1314
+ {isReady ? (activeTab === 'insights' ? (<ScrollView style={styles.insightsContainer} contentContainerStyle={styles.insightsContent} showsVerticalScrollIndicator={false}>
1315
+ {renderInsightsDashboard()}
1316
+ </ScrollView>) : activeTab === 'analytics' ? (selectedEvent != null ? (<AnalyticsDetail event={selectedEvent}/>) : analyticsSubTab === 'top_events' ? (<FlatList data={topEventsArray} keyExtractor={item => item[0]} contentContainerStyle={[
1185
1317
  styles.listContent,
1186
1318
  { paddingHorizontal: 16, paddingTop: 16 },
1187
1319
  ]} renderItem={({ item: [name, count] }) => {
1188
1320
  const maxCount = topEventsArray[0]?.[1] || 1;
1189
1321
  const color = getEventColor(name);
1190
1322
  return (<View style={[
1191
- analyticsListStyles.topEventsCard,
1323
+ styles.analyticsTopEventsCard,
1192
1324
  { marginBottom: 12, paddingVertical: 16 },
1193
1325
  ]}>
1194
- <View style={analyticsListStyles.topEventRow}>
1326
+ <View style={styles.analyticsTopEventRow}>
1195
1327
  <View style={{
1196
1328
  flexDirection: 'row',
1197
1329
  alignItems: 'center',
@@ -1199,7 +1331,7 @@ const NetworkInspector = () => {
1199
1331
  flex: 1,
1200
1332
  }}>
1201
1333
  <View style={[
1202
- analyticsListStyles.iconCircle,
1334
+ styles.analyticsIconCircle,
1203
1335
  { backgroundColor: `${color}1A` },
1204
1336
  ]}>
1205
1337
  <Svg width={14} height={14} viewBox="0 0 24 24" fill={color}>
@@ -1207,19 +1339,19 @@ const NetworkInspector = () => {
1207
1339
  <Path d="M7 14l3-3 4 4 6-6" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
1208
1340
  </Svg>
1209
1341
  </View>
1210
- <Text style={analyticsListStyles.topEventName} numberOfLines={2}>
1342
+ <Text style={styles.analyticsTopEventName} numberOfLines={2}>
1211
1343
  {name}
1212
1344
  </Text>
1213
1345
  </View>
1214
- <View style={analyticsListStyles.topEventBarWrap}>
1346
+ <View style={styles.analyticsTopEventBarWrap}>
1215
1347
  <LinearGradient colors={[color, `${color}99`]} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={[
1216
- analyticsListStyles.topEventBar,
1348
+ styles.analyticsTopEventBar,
1217
1349
  {
1218
1350
  width: `${Math.max(6, (count / maxCount) * 100)}%`,
1219
1351
  },
1220
1352
  ]}/>
1221
1353
  </View>
1222
- <Text style={analyticsListStyles.topEventCount}>
1354
+ <Text style={styles.analyticsTopEventCount}>
1223
1355
  {count}
1224
1356
  </Text>
1225
1357
  </View>
@@ -1229,39 +1361,7 @@ const NetworkInspector = () => {
1229
1361
  <EmptyRadarIcon color={AppColors.purple} size={32}/>
1230
1362
  </View>
1231
1363
  <Text style={styles.emptyTitle}>No Top Events</Text>
1232
- </View>}/>) : groupByScreen ? (<SectionList sections={groupedAnalyticsEvents} keyExtractor={item => item.id.toString()} renderSectionHeader={renderScreenSectionHeader} renderItem={({ item, index, section }) => {
1233
- if (expandedScreens.has(section.title))
1234
- return null;
1235
- const events = section.data;
1236
- const prev = events[index + 1];
1237
- const next = events[index - 1];
1238
- const msSincePrev = prev
1239
- ? item.timestamp - prev.timestamp
1240
- : undefined;
1241
- const thisMin = Math.floor(item.timestamp / 60000);
1242
- const nextMin = next
1243
- ? Math.floor(next.timestamp / 60000)
1244
- : -1;
1245
- const showTimestamp = index === 0 || thisMin !== nextMin;
1246
- return (<AnalyticsEventCard event={item} onPress={() => setSelectedEvent(item)} isNew={newEventIds.has(item.id)} searchStr={analyticsSearch} isFirst={index === 0} isLast={index === events.length - 1} msSincePrev={msSincePrev} showTimestamp={showTimestamp} computedScreenName={section.title}/>);
1247
- }} initialNumToRender={20} maxToRenderPerBatch={20} windowSize={5} removeClippedSubviews ListHeaderComponent={null} ListEmptyComponent={<View style={styles.emptyContainer}>
1248
- <View style={styles.emptyIconWrap}>
1249
- <EmptyRadarIcon color={AppColors.purple} size={32}/>
1250
- </View>
1251
- <Text style={styles.emptyTitle}>
1252
- {analyticsSearch.length > 0
1253
- ? 'No matching events'
1254
- : 'No analytics events yet'}
1255
- </Text>
1256
- <Text style={styles.emptySub}>
1257
- {analyticsSearch.length > 0
1258
- ? 'Try adjusting your search.'
1259
- : 'Call setupAnalyticsLogger(analytics()) at app start.'}
1260
- </Text>
1261
- </View>} contentContainerStyle={[
1262
- styles.listContent,
1263
- filteredAnalyticsEvents.length === 0 && { flexGrow: 1 },
1264
- ]} keyboardShouldPersistTaps="handled"/>) : (<FlatList data={filteredAnalyticsEvents} keyExtractor={item => item.id.toString()} renderItem={({ item, index }) => {
1364
+ </View>}/>) : (<FlatList data={filteredAnalyticsEvents} keyExtractor={item => item.id.toString()} renderItem={({ item, index }) => {
1265
1365
  const prev = filteredAnalyticsEvents[index + 1];
1266
1366
  const next = filteredAnalyticsEvents[index - 1];
1267
1367
  const msSincePrev = prev
@@ -1291,7 +1391,7 @@ const NetworkInspector = () => {
1291
1391
  }
1292
1392
  return screenName;
1293
1393
  })()}/>);
1294
- }} initialNumToRender={20} maxToRenderPerBatch={20} windowSize={5} removeClippedSubviews ListHeaderComponent={null} ListEmptyComponent={<View style={styles.emptyContainer}>
1394
+ }} initialNumToRender={20} maxToRenderPerBatch={20} windowSize={5} removeClippedSubviews ListEmptyComponent={<View style={styles.emptyContainer}>
1295
1395
  <View style={styles.emptyIconWrap}>
1296
1396
  <EmptyRadarIcon color={AppColors.purple} size={32}/>
1297
1397
  </View>
@@ -1308,84 +1408,7 @@ const NetworkInspector = () => {
1308
1408
  </View>} contentContainerStyle={[
1309
1409
  styles.listContent,
1310
1410
  filteredAnalyticsEvents.length === 0 && { flexGrow: 1 },
1311
- ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (groupByScreen ? (<SectionList sections={groupedNetworkLogs} keyExtractor={item => item?.id?.toString()} renderSectionHeader={renderScreenSectionHeader} renderItem={({ item, section }) => {
1312
- if (expandedScreens.has(section.title))
1313
- return null;
1314
- return renderItem({
1315
- item: {
1316
- type: 'log',
1317
- log: item,
1318
- color: AppColors.purple,
1319
- },
1320
- });
1321
- }} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
1322
- {logs.length > 0 && (<View style={styles.dashboardCard}>
1323
- <View style={styles.dashboardStatsRow}>
1324
- <View style={styles.statBox}>
1325
- <Text style={styles.statValue}>
1326
- {stats.filtered < stats.total
1327
- ? stats.filtered
1328
- : stats.total}
1329
- </Text>
1330
- <Text style={styles.statLabel}>
1331
- {stats.filtered < stats.total
1332
- ? `of ${stats.total} Req`
1333
- : 'Requests'}
1334
- </Text>
1335
- <View style={styles.miniGraphWrap}>
1336
- <MiniBarChart data={stats.reqTrend} color={AppColors.purple}/>
1337
- </View>
1338
- </View>
1339
- <View style={styles.dashboardStatDivider}/>
1340
- <View style={styles.statBox}>
1341
- <Text style={[
1342
- styles.statValue,
1343
- stats.errors > 0 && {
1344
- color: AppColors.errorColor,
1345
- },
1346
- ]}>
1347
- {stats.errors}
1348
- </Text>
1349
- <Text style={styles.statLabel}>Errors</Text>
1350
- <View style={styles.miniGraphWrap}>
1351
- <MiniBarChart data={stats.errorTrend} color={AppColors.errorColor} maxVal={1}/>
1352
- </View>
1353
- </View>
1354
- <View style={styles.dashboardStatDivider}/>
1355
- <View style={styles.statBox}>
1356
- <Text style={[
1357
- styles.statValue,
1358
- stats.avgDuration != null
1359
- ? {
1360
- color: getDurationColor(stats.avgDuration),
1361
- }
1362
- : null,
1363
- ]}>
1364
- {stats.avgDuration != null
1365
- ? `${stats.avgDuration}ms`
1366
- : '—'}
1367
- </Text>
1368
- <Text style={styles.statLabel}>Avg Time</Text>
1369
- <View style={styles.miniGraphWrap}>
1370
- <MiniLineChart data={stats.durationTrend} color={AppColors.darkOrange}/>
1371
- </View>
1372
- </View>
1373
- <View style={styles.dashboardStatDivider}/>
1374
- <View style={styles.statBox}>
1375
- <Text style={[
1376
- styles.statValue,
1377
- { color: AppColors.skyBlue },
1378
- ]}>
1379
- {stats.size}
1380
- </Text>
1381
- <Text style={styles.statLabel}>Payload</Text>
1382
- <View style={styles.miniGraphWrap}>
1383
- <MiniBarChart data={stats.sizeTrend} color={AppColors.skyBlue}/>
1384
- </View>
1385
- </View>
1386
- </View>
1387
- </View>)}
1388
-
1411
+ ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'apis' && selected == null ? (<FlatList data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
1389
1412
  <View style={styles.toolbarRow}>
1390
1413
  <View style={styles.searchContainer}>
1391
1414
  <SearchIcon color={AppColors.grayTextWeak} size={16}/>
@@ -1405,14 +1428,6 @@ const NetworkInspector = () => {
1405
1428
  </View>)}
1406
1429
  </TouchableScale>
1407
1430
 
1408
- <TouchableScale style={[
1409
- styles.toolbarBtn,
1410
- groupByScreen && styles.toolbarBtnActive,
1411
- ]} onPress={() => setGroupByScreen(prev => !prev)} hitSlop={10}>
1412
- <MapPinIcon color={groupByScreen
1413
- ? AppColors.purple
1414
- : AppColors.grayTextStrong} size={18}/>
1415
- </TouchableScale>
1416
1431
 
1417
1432
  <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
1418
1433
  <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
@@ -1456,237 +1471,22 @@ const NetworkInspector = () => {
1456
1471
  });
1457
1472
  }
1458
1473
  }} hitSlop={10}>
1459
- {active ? (<LinearGradient colors={[
1460
- AppColors.purpleShade50,
1461
- '#EAE5FF',
1462
- ]} style={[
1474
+ {active ? (<View style={[
1463
1475
  styles.statusFilterChip,
1464
1476
  styles.statusFilterActive,
1477
+ { overflow: 'hidden' },
1465
1478
  ]}>
1466
- <Text style={[
1467
- styles.statusFilterText,
1468
- { color: AppColors.purple },
1469
- ]}>
1470
- {filter}
1471
- </Text>
1472
- </LinearGradient>) : (<View style={styles.statusFilterChip}>
1473
- <Text style={styles.statusFilterText}>
1474
- {filter}
1475
- </Text>
1476
- </View>)}
1477
- </TouchableScale>);
1478
- })}
1479
- </ScrollView>
1480
-
1481
- <Text style={[styles.filtersHeading, { marginTop: 16 }]}>
1482
- METHOD
1483
- </Text>
1484
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
1485
- {availableMethods.map(filter => {
1486
- const isAll = filter === 'ALL';
1487
- const active = isAll
1488
- ? methodFilters.size === 0
1489
- : methodFilters.has(filter);
1490
- return (<TouchableScale key={filter} style={styles.statusFilterWrap} onPress={() => {
1491
- if (isAll) {
1492
- setMethodFilters(new Set());
1493
- }
1494
- else {
1495
- setMethodFilters(prev => {
1496
- const next = new Set(prev);
1497
- next.has(filter)
1498
- ? next.delete(filter)
1499
- : next.add(filter);
1500
- return next;
1501
- });
1502
- }
1503
- }} hitSlop={10}>
1504
- {active ? (<LinearGradient colors={[
1505
- AppColors.purpleShade50,
1506
- '#EAE5FF',
1507
- ]} style={[
1508
- styles.statusFilterChip,
1509
- styles.statusFilterActive,
1510
- ]}>
1511
- <Text style={[
1512
- styles.statusFilterText,
1513
- { color: AppColors.purple },
1514
- ]}>
1515
- {filter}
1516
- </Text>
1517
- </LinearGradient>) : (<View style={styles.statusFilterChip}>
1518
- <Text style={styles.statusFilterText}>
1519
- {filter}
1520
- </Text>
1521
- </View>)}
1522
- </TouchableScale>);
1523
- })}
1524
- </ScrollView>
1525
- </View>
1526
- </Animated.View>
1527
-
1528
- {(search ||
1529
- statusFilters.size > 0 ||
1530
- methodFilters.size > 0) && (<Text style={styles.resultCount}>
1531
- {filteredLogs.length === logs.length
1532
- ? `${logs.length} requests`
1533
- : `${filteredLogs.length} of ${logs.length} filtered requests`}
1534
- </Text>)}
1535
- </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
1536
- styles.listContent,
1537
- filteredLogs.length === 0 && { flexGrow: 1 },
1538
- ]} keyboardShouldPersistTaps="handled"/>) : (<FlatList data={groupedData} keyExtractor={item => item?.id?.toString()} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={true} ListHeaderComponent={<View style={{ marginTop: 8 }}>
1539
- {logs.length > 0 && (<View style={styles.dashboardCard}>
1540
- <View style={styles.dashboardStatsRow}>
1541
- <View style={styles.statBox}>
1542
- <Text style={styles.statValue}>
1543
- {stats.filtered < stats.total
1544
- ? stats.filtered
1545
- : stats.total}
1546
- </Text>
1547
- <Text style={styles.statLabel}>
1548
- {stats.filtered < stats.total
1549
- ? `of ${stats.total} Req`
1550
- : 'Requests'}
1551
- </Text>
1552
- <View style={styles.miniGraphWrap}>
1553
- <MiniBarChart data={stats.reqTrend} color={AppColors.purple}/>
1554
- </View>
1555
- </View>
1556
- <View style={styles.dashboardStatDivider}/>
1557
- <View style={styles.statBox}>
1558
- <Text style={[
1559
- styles.statValue,
1560
- stats.errors > 0 && {
1561
- color: AppColors.errorColor,
1562
- },
1563
- ]}>
1564
- {stats.errors}
1565
- </Text>
1566
- <Text style={styles.statLabel}>Errors</Text>
1567
- <View style={styles.miniGraphWrap}>
1568
- <MiniBarChart data={stats.errorTrend} color={AppColors.errorColor} maxVal={1}/>
1569
- </View>
1570
- </View>
1571
- <View style={styles.dashboardStatDivider}/>
1572
- <View style={styles.statBox}>
1573
- <Text style={[
1574
- styles.statValue,
1575
- stats.avgDuration != null
1576
- ? {
1577
- color: getDurationColor(stats.avgDuration),
1578
- }
1579
- : null,
1580
- ]}>
1581
- {stats.avgDuration != null
1582
- ? `${stats.avgDuration}ms`
1583
- : '—'}
1584
- </Text>
1585
- <Text style={styles.statLabel}>Avg Time</Text>
1586
- <View style={styles.miniGraphWrap}>
1587
- <MiniLineChart data={stats.durationTrend} color={AppColors.darkOrange}/>
1588
- </View>
1589
- </View>
1590
- <View style={styles.dashboardStatDivider}/>
1591
- <View style={styles.statBox}>
1592
- <Text style={[
1593
- styles.statValue,
1594
- { color: AppColors.skyBlue },
1595
- ]}>
1596
- {stats.size}
1597
- </Text>
1598
- <Text style={styles.statLabel}>Payload</Text>
1599
- <View style={styles.miniGraphWrap}>
1600
- <MiniBarChart data={stats.sizeTrend} color={AppColors.skyBlue}/>
1601
- </View>
1602
- </View>
1603
- </View>
1604
- </View>)}
1605
-
1606
- <View style={styles.toolbarRow}>
1607
- <View style={styles.searchContainer}>
1608
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
1609
- <TextInput placeholder="Search endpoints..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={setSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
1610
- {search.length > 0 && (<Pressable onPress={() => setSearch('')} hitSlop={10} style={styles.clearBtn}>
1611
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
1612
- </Pressable>)}
1613
- </View>
1614
-
1615
- <View style={styles.toolbarRight}>
1616
- <TouchableScale style={styles.toolbarBtn} onPress={handleDelete} hitSlop={10}>
1617
- <TrashIcon color={AppColors.grayTextStrong} size={18}/>
1618
- {selectedLogs.size > 0 && (<View style={styles.trashBadge}>
1619
- <Text style={styles.trashBadgeText}>
1620
- {selectedLogs.size}
1621
- </Text>
1622
- </View>)}
1623
- </TouchableScale>
1624
-
1625
- <TouchableScale style={[
1626
- styles.toolbarBtn,
1627
- groupByScreen && styles.toolbarBtnActive,
1628
- ]} onPress={() => setGroupByScreen(prev => !prev)} hitSlop={10}>
1629
- <MapPinIcon color={groupByScreen
1630
- ? AppColors.purple
1631
- : AppColors.grayTextStrong} size={18}/>
1632
- </TouchableScale>
1633
-
1634
- <TouchableScale style={styles.toolbarBtn} onPress={() => setSortOrder(o => o === 'newest' ? 'oldest' : 'newest')} hitSlop={10}>
1635
- <SortArrowIcon direction={sortOrder === 'newest' ? 'down' : 'up'} color={AppColors.grayTextStrong} size={18}/>
1636
- </TouchableScale>
1637
-
1638
- <TouchableScale style={[
1639
- styles.toolbarBtn,
1640
- filtersAccordion.isOpen &&
1641
- styles.toolbarBtnActive,
1642
- ]} onPress={filtersAccordion.toggleOpen} hitSlop={10}>
1643
- <FilterIcon color={filtersAccordion.isOpen
1644
- ? AppColors.purple
1645
- : AppColors.grayTextStrong} size={18}/>
1646
- </TouchableScale>
1647
- </View>
1648
- </View>
1649
-
1650
- <Animated.View style={[
1651
- filtersAccordion.bodyStyle,
1652
- { overflow: 'hidden' },
1653
- ]}>
1654
- <View style={styles.filtersContainer}>
1655
- <Text style={styles.filtersHeading}>STATUS</Text>
1656
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.statusRowContent}>
1657
- {STATUS_FILTERS.map(filter => {
1658
- const isAll = filter === 'ALL';
1659
- const active = isAll
1660
- ? statusFilters.size === 0
1661
- : statusFilters.has(filter);
1662
- return (<TouchableScale key={filter} style={styles.statusFilterWrap} onPress={() => {
1663
- if (isAll) {
1664
- setStatusFilters(new Set());
1665
- }
1666
- else {
1667
- setStatusFilters(prev => {
1668
- const next = new Set(prev);
1669
- next.has(filter)
1670
- ? next.delete(filter)
1671
- : next.add(filter);
1672
- return next;
1673
- });
1674
- }
1675
- }} hitSlop={10}>
1676
- {active ? (<LinearGradient colors={[
1479
+ <LinearGradient colors={[
1677
1480
  AppColors.purpleShade50,
1678
1481
  '#EAE5FF',
1679
- ]} style={[
1680
- styles.statusFilterChip,
1681
- styles.statusFilterActive,
1682
- ]}>
1482
+ ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
1683
1483
  <Text style={[
1684
1484
  styles.statusFilterText,
1685
1485
  { color: AppColors.purple },
1686
1486
  ]}>
1687
1487
  {filter}
1688
1488
  </Text>
1689
- </LinearGradient>) : (<View style={styles.statusFilterChip}>
1489
+ </View>) : (<View style={styles.statusFilterChip}>
1690
1490
  <Text style={styles.statusFilterText}>
1691
1491
  {filter}
1692
1492
  </Text>
@@ -1718,20 +1518,22 @@ const NetworkInspector = () => {
1718
1518
  });
1719
1519
  }
1720
1520
  }} hitSlop={10}>
1721
- {active ? (<LinearGradient colors={[
1722
- AppColors.purpleShade50,
1723
- '#EAE5FF',
1724
- ]} style={[
1521
+ {active ? (<View style={[
1725
1522
  styles.statusFilterChip,
1726
1523
  styles.statusFilterActive,
1524
+ { overflow: 'hidden' },
1727
1525
  ]}>
1526
+ <LinearGradient colors={[
1527
+ AppColors.purpleShade50,
1528
+ '#EAE5FF',
1529
+ ]} style={StyleSheet.absoluteFill} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }}/>
1728
1530
  <Text style={[
1729
1531
  styles.statusFilterText,
1730
1532
  { color: AppColors.purple },
1731
1533
  ]}>
1732
1534
  {filter}
1733
1535
  </Text>
1734
- </LinearGradient>) : (<View style={styles.statusFilterChip}>
1536
+ </View>) : (<View style={styles.statusFilterChip}>
1735
1537
  <Text style={styles.statusFilterText}>
1736
1538
  {filter}
1737
1539
  </Text>
@@ -1752,7 +1554,7 @@ const NetworkInspector = () => {
1752
1554
  </View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 || statusFilters.size > 0}/>} contentContainerStyle={[
1753
1555
  styles.listContent,
1754
1556
  filteredLogs.length === 0 && { flexGrow: 1 },
1755
- ]} keyboardShouldPersistTaps="handled"/>)) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
1557
+ ]} keyboardShouldPersistTaps="handled"/>) : activeTab === 'logs' ? (<View style={{ flex: 1 }}>
1756
1558
  <View style={{
1757
1559
  backgroundColor: '#FFFFFF',
1758
1560
  borderBottomWidth: 1,
@@ -2199,78 +2001,23 @@ const NetworkInspector = () => {
2199
2001
  })}
2200
2002
  </View>
2201
2003
  <View style={{ flex: 1, padding: 12 }}>
2202
- {htmlSubTab === 'html' ? (webViewHtml ? (<View style={{ flex: 1 }}>
2203
- <View style={{
2204
- flexDirection: 'row',
2205
- alignItems: 'center',
2206
- marginBottom: 8,
2207
- gap: 8,
2208
- }}>
2209
- <View style={[
2210
- styles.searchContainer,
2211
- { flex: 1 },
2212
- ]}>
2213
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
2214
- <TextInput placeholder="Search HTML..." placeholderTextColor={AppColors.grayTextWeak} value={htmlSearch} onChangeText={setHtmlSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
2215
- {htmlSearch.length > 0 && (<Pressable onPress={() => setHtmlSearch('')} hitSlop={10} style={styles.clearBtn}>
2216
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2217
- </Pressable>)}
2218
- </View>
2219
- <CopyButton value={webViewHtml} label="HTML Source"/>
2220
- </View>
2221
- <CodeSnippet code={webViewHtml} language="html" search={htmlSearch}/>
2222
- </View>) : (<Text style={{
2004
+ {!isHtmlTabReady ? (<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', minHeight: 200 }}>
2005
+ <ActivityIndicator size="large" color={AppColors.purple}/>
2006
+ </View>) : htmlSubTab === 'html' ? (webViewHtml ? (<CodeSnippet code={webViewHtml} language="html"/>) : (<Text style={{
2223
2007
  fontFamily: 'monospace',
2224
2008
  fontSize: 11,
2225
2009
  color: '#94A3B8',
2226
2010
  padding: 12,
2227
2011
  }}>
2228
2012
  No HTML content captured.
2229
- </Text>)) : htmlSubTab === 'css' ? (webViewCss ? (<View style={{ flex: 1 }}>
2230
- <View style={{
2231
- flexDirection: 'row',
2232
- alignItems: 'center',
2233
- marginBottom: 8,
2234
- gap: 8,
2235
- }}>
2236
- <View style={[
2237
- styles.searchContainer,
2238
- { flex: 1 },
2239
- ]}>
2240
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
2241
- <TextInput placeholder="Search CSS..." placeholderTextColor={AppColors.grayTextWeak} value={cssSearch} onChangeText={setCssSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
2242
- {cssSearch.length > 0 && (<Pressable onPress={() => setCssSearch('')} hitSlop={10} style={styles.clearBtn}>
2243
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2244
- </Pressable>)}
2245
- </View>
2246
- <CopyButton value={webViewCss} label="CSS Source"/>
2247
- </View>
2248
- <CodeSnippet code={webViewCss} language="css" search={cssSearch}/>
2249
- </View>) : (<Text style={{
2013
+ </Text>)) : htmlSubTab === 'css' ? (webViewCss ? (<CodeSnippet code={webViewCss} language="css"/>) : (<Text style={{
2250
2014
  fontFamily: 'monospace',
2251
2015
  fontSize: 11,
2252
2016
  color: '#94A3B8',
2253
2017
  padding: 12,
2254
2018
  }}>
2255
2019
  No CSS styles detected on this page.
2256
- </Text>)) : webViewJs ? (<View style={{ flex: 1 }}>
2257
- <View style={{
2258
- flexDirection: 'row',
2259
- alignItems: 'center',
2260
- marginBottom: 8,
2261
- gap: 8,
2262
- }}>
2263
- <View style={[styles.searchContainer, { flex: 1 }]}>
2264
- <SearchIcon color={AppColors.grayTextWeak} size={16}/>
2265
- <TextInput placeholder="Search Javascript..." placeholderTextColor={AppColors.grayTextWeak} value={jsSearch} onChangeText={setJsSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
2266
- {jsSearch.length > 0 && (<Pressable onPress={() => setJsSearch('')} hitSlop={10} style={styles.clearBtn}>
2267
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2268
- </Pressable>)}
2269
- </View>
2270
- <CopyButton value={webViewJs} label="JS Source"/>
2271
- </View>
2272
- <CodeSnippet code={webViewJs} language="javascript" search={jsSearch}/>
2273
- </View>) : (<Text style={{
2020
+ </Text>)) : webViewJs ? (<CodeSnippet code={webViewJs} language="javascript"/>) : (<Text style={{
2274
2021
  fontFamily: 'monospace',
2275
2022
  fontSize: 11,
2276
2023
  color: '#94A3B8',
@@ -2366,60 +2113,89 @@ const NetworkInspector = () => {
2366
2113
  styles.listContent,
2367
2114
  filteredNavHistory.length === 0 && { flexGrow: 1 },
2368
2115
  ]} keyboardShouldPersistTaps="handled"/>)}
2369
- </View>) : (<ScrollView style={styles.detailScroll} contentContainerStyle={styles.detailContent} showsVerticalScrollIndicator={true}>
2370
- {(() => {
2116
+ </View>) : activeTab === 'redux' ? (renderReduxTab()) : (<View style={{ flex: 1 }}>
2117
+ {/* Non-scrollable details header */}
2118
+ <View style={{ paddingHorizontal: 6, paddingTop: 4 }}>
2119
+ {(() => {
2371
2120
  const routeInfo = logRouteMapRef.current.get(selected.id);
2372
2121
  const screenPath = routeInfo && routeInfo.path !== 'Navigators'
2373
2122
  ? routeInfo.path.split(' ➔ ')
2374
2123
  : [];
2375
- const parts = ['APIs', 'APIs', ...screenPath];
2376
- return (<View style={{ marginBottom: 12, marginTop: 4 }}>
2377
- <Text style={{
2378
- fontFamily: AppFonts.interMedium,
2379
- fontSize: 11.5,
2380
- color: AppColors.grayTextWeak,
2124
+ const parts = ['APIs', ...screenPath];
2125
+ return (<View style={{
2126
+ flexDirection: 'row',
2127
+ alignItems: 'center',
2128
+ backgroundColor: AppColors.primaryLight,
2129
+ borderRadius: 8,
2130
+ paddingVertical: 8,
2131
+ paddingHorizontal: 12,
2132
+ borderWidth: 1,
2133
+ borderColor: AppColors.dividerColor,
2134
+ marginBottom: 12,
2135
+ marginTop: 4,
2381
2136
  }}>
2382
- {parts.map((part, index) => (<React.Fragment key={index}>
2383
- {index > 0 && ''}
2384
- <Text style={index === parts.length - 1
2385
- ? {
2386
- color: AppColors.purple,
2387
- fontFamily: AppFonts.interBold,
2388
- }
2389
- : undefined}>
2390
- {part}
2391
- </Text>
2392
- </React.Fragment>))}
2393
- </Text>
2394
- </View>);
2137
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
2138
+ flexDirection: 'row',
2139
+ alignItems: 'center',
2140
+ gap: 6,
2141
+ }}>
2142
+ {parts.map((part, index) => {
2143
+ const isLast = index === parts.length - 1;
2144
+ return (<React.Fragment key={index}>
2145
+ {index > 0 && (<Text style={{ color: AppColors.grayTextWeak, fontSize: 11, marginHorizontal: 2 }}>
2146
+ /
2147
+ </Text>)}
2148
+ <View style={isLast
2149
+ ? {
2150
+ backgroundColor: `${AppColors.purple}12`,
2151
+ paddingHorizontal: 8,
2152
+ paddingVertical: 3,
2153
+ borderRadius: 6,
2154
+ }
2155
+ : {
2156
+ paddingHorizontal: 4,
2157
+ paddingVertical: 2,
2158
+ }}>
2159
+ <Text style={{
2160
+ fontFamily: isLast ? AppFonts.interBold : AppFonts.interMedium,
2161
+ fontSize: 11.5,
2162
+ color: isLast ? AppColors.purple : AppColors.grayText,
2163
+ }}>
2164
+ {part}
2165
+ </Text>
2166
+ </View>
2167
+ </React.Fragment>);
2168
+ })}
2169
+ </ScrollView>
2170
+ </View>);
2395
2171
  })()}
2396
2172
 
2397
- <View style={styles.detailInfoBar}>
2398
- <View style={styles.detailInfoTop}>
2399
- <View style={{
2173
+ <View style={styles.detailInfoBar}>
2174
+ <View style={styles.detailInfoTop}>
2175
+ <View style={{
2400
2176
  flexDirection: 'row',
2401
2177
  alignItems: 'center',
2402
2178
  gap: 10,
2403
2179
  }}>
2404
- <View style={[
2180
+ <View style={[
2405
2181
  styles.methodBadge,
2406
2182
  {
2407
2183
  backgroundColor: `${METHOD_COLORS[selected.method] ??
2408
2184
  METHOD_COLORS.ALL}15`,
2409
2185
  },
2410
2186
  ]}>
2411
- <Text style={[
2187
+ <Text style={[
2412
2188
  styles.methodBadgeText,
2413
2189
  {
2414
2190
  color: METHOD_COLORS[selected.method] ??
2415
2191
  METHOD_COLORS.ALL,
2416
2192
  },
2417
2193
  ]}>
2418
- {selected.method}
2419
- </Text>
2420
- </View>
2194
+ {selected.method}
2195
+ </Text>
2196
+ </View>
2421
2197
 
2422
- {selected.status != null && (<View style={[
2198
+ {selected.status != null && (<View style={[
2423
2199
  styles.chip,
2424
2200
  {
2425
2201
  backgroundColor: selected.status === 0
@@ -2430,10 +2206,10 @@ const NetworkInspector = () => {
2430
2206
  : `${getStatusColor(selected.status)}40`,
2431
2207
  },
2432
2208
  ]}>
2433
- {selected.status === 0 ? (<FailIcon size={8} color={AppColors.errorColor}/>) : (<Svg width={6} height={6} viewBox="0 0 10 10" fill="none">
2434
- <Circle cx="5" cy="5" r="5" fill={getStatusColor(selected.status)}/>
2435
- </Svg>)}
2436
- <Text style={[
2209
+ {selected.status === 0 ? (<FailIcon size={8} color={AppColors.errorColor}/>) : (<Svg width={6} height={6} viewBox="0 0 10 10" fill="none">
2210
+ <Circle cx="5" cy="5" r="5" fill={getStatusColor(selected.status)}/>
2211
+ </Svg>)}
2212
+ <Text style={[
2437
2213
  styles.chipText,
2438
2214
  {
2439
2215
  color: selected.status === 0
@@ -2441,183 +2217,176 @@ const NetworkInspector = () => {
2441
2217
  : getStatusColor(selected.status),
2442
2218
  },
2443
2219
  ]}>
2444
- {selected.status === 0
2220
+ {selected.status === 0
2445
2221
  ? 'Failed'
2446
2222
  : String(selected.status)}
2447
- </Text>
2448
- </View>)}
2449
- </View>
2450
- <View style={styles.detailInfoRight}>
2451
- <TouchableScale style={styles.iconSquareBtn} onPress={() => Linking.openURL(detailDisplayUrl)} hitSlop={12}>
2452
- <GlobeIcon color={AppColors.grayTextWeak} size={14}/>
2453
- </TouchableScale>
2454
- <CopyButton value={getFetchCommand(selected)} label="fetch()" iconType="fetch"/>
2455
- <CopyButton value={getCurlCommand(selected)} label="cURL" iconType="terminal"/>
2456
- <CopyButton value={detailDisplayUrl} label="URL"/>
2223
+ </Text>
2224
+ </View>)}
2225
+ </View>
2226
+ <View style={styles.detailInfoRight}>
2227
+ <TouchableScale style={styles.iconSquareBtn} onPress={() => Linking.openURL(detailDisplayUrl)} hitSlop={12}>
2228
+ <GlobeIcon color={AppColors.grayTextWeak} size={14}/>
2229
+ </TouchableScale>
2230
+ <CopyButton value={getFetchCommand(selected)} label="fetch()" iconType="fetch"/>
2231
+ <CopyButton value={getCurlCommand(selected)} label="cURL" iconType="terminal"/>
2232
+ <CopyButton value={detailDisplayUrl} label="URL"/>
2233
+ </View>
2457
2234
  </View>
2235
+
2236
+ <Pressable style={styles.detailUrlContainer} onPress={() => Linking.openURL(detailDisplayUrl)}>
2237
+ <Text selectable={true} style={styles.detailUrl}>
2238
+ {detailDisplayUrl}
2239
+ </Text>
2240
+ </Pressable>
2458
2241
  </View>
2242
+ </View>
2459
2243
 
2460
- <Pressable style={styles.detailUrlContainer} onPress={() => Linking.openURL(detailDisplayUrl)}>
2461
- <Text selectable={true} style={styles.detailUrl}>
2462
- {detailDisplayUrl}
2463
- </Text>
2464
- </Pressable>
2244
+ {/* Sticky Segment Control */}
2245
+ <View style={{
2246
+ flexDirection: 'row',
2247
+ backgroundColor: AppColors.grayBackground,
2248
+ borderRadius: 10,
2249
+ padding: 3,
2250
+ marginHorizontal: 6,
2251
+ marginBottom: 10,
2252
+ marginTop: 6,
2253
+ borderWidth: 1,
2254
+ borderColor: AppColors.dividerColor,
2255
+ }}>
2256
+ {['metadata', 'headers', 'request', 'response'].map(tab => {
2257
+ const isActive = apiDetailActiveTab === tab;
2258
+ if (tab === 'request' && selected.request == null)
2259
+ return null;
2260
+ const getLabel = () => {
2261
+ if (tab === 'metadata')
2262
+ return 'Metadata';
2263
+ if (tab === 'headers')
2264
+ return 'Headers';
2265
+ if (tab === 'request')
2266
+ return 'Request';
2267
+ return 'Response';
2268
+ };
2269
+ return (<TouchableOpacity key={tab} onPress={() => setApiDetailActiveTab(tab)} style={{
2270
+ flex: 1,
2271
+ paddingVertical: 6,
2272
+ alignItems: 'center',
2273
+ borderRadius: 8,
2274
+ backgroundColor: isActive ? AppColors.purple : 'transparent',
2275
+ }}>
2276
+ <Text style={{
2277
+ fontFamily: AppFonts.interBold,
2278
+ fontSize: 11,
2279
+ color: isActive ? '#FFFFFF' : AppColors.grayText,
2280
+ }}>
2281
+ {getLabel()}
2282
+ </Text>
2283
+ </TouchableOpacity>);
2284
+ })}
2465
2285
  </View>
2466
2286
 
2467
- <MetaAccordion status={selected.status} statusColor={getStatusColor(selected.status)} duration={selected.duration} size={getSize(selected.response)} triggeredAt={formatDateTime(selected.startTime)}/>
2287
+ {/* Scrollable Tab Content */}
2288
+ <ScrollView style={styles.detailScroll} contentContainerStyle={{ paddingHorizontal: 6, paddingBottom: 24 }} showsVerticalScrollIndicator={true}>
2289
+ {apiDetailActiveTab === 'metadata' && (<>
2290
+ <MetaAccordion status={selected.status} statusColor={getStatusColor(selected.status)} duration={selected.duration} size={getSize(selected.response)} triggeredAt={formatDateTime(selected.startTime)}/>
2468
2291
 
2469
- {(() => {
2470
- const routeInfo = logRouteMapRef.current.get(selected.id);
2471
- if (!routeInfo || routeInfo.path === 'Navigators')
2472
- return null;
2473
- return <SourcePageCard routeInfo={routeInfo}/>;
2474
- })()}
2292
+ {(() => {
2293
+ const routeInfo = logRouteMapRef.current.get(selected.id);
2294
+ if (!routeInfo || routeInfo.path === 'Navigators')
2295
+ return null;
2296
+ return <SourcePageCard routeInfo={routeInfo}/>;
2297
+ })()}
2475
2298
 
2476
- <View style={styles.seperator}/>
2299
+ {(() => {
2300
+ const cType = selected.responseHeaders?.['content-type'] ||
2301
+ selected.responseHeaders?.['Content-Type'];
2302
+ if (cType?.includes('image/')) {
2303
+ return (<View style={styles.imagePreviewWrapper}>
2304
+ <Image source={{ uri: selected.url }} style={styles.imagePreview} resizeMode="contain"/>
2305
+ <TouchableScale style={styles.imageDownloadBtn} onPress={() => Linking.openURL(selected.url)} hitSlop={10}>
2306
+ <DownloadIcon color={AppColors.purple} size={18}/>
2307
+ </TouchableScale>
2308
+ </View>);
2309
+ }
2310
+ return null;
2311
+ })()}
2312
+ </>)}
2477
2313
 
2478
- {(() => {
2479
- const cType = selected.responseHeaders?.['content-type'] ||
2480
- selected.responseHeaders?.['Content-Type'];
2481
- if (cType?.includes('image/')) {
2482
- return (<View style={styles.imagePreviewWrapper}>
2483
- <Image source={{ uri: selected.url }} style={styles.imagePreview} resizeMode="contain"/>
2484
- <TouchableScale style={styles.imageDownloadBtn} onPress={() => Linking.openURL(selected.url)} hitSlop={10}>
2485
- <DownloadIcon color={AppColors.purple} size={18}/>
2486
- </TouchableScale>
2487
- </View>);
2488
- }
2489
- return null;
2490
- })()}
2314
+ {apiDetailActiveTab === 'headers' && (<>
2315
+ <View style={styles.detailSearchRow}>
2316
+ <View style={styles.detailSearchBox}>
2317
+ <TextInput placeholder="Search headers..." placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={styles.detailSearchInput} autoCorrect={false} autoCapitalize="none"/>
2318
+ {detailSearch.length > 0 && (<Pressable onPress={() => setDetailSearch('')} hitSlop={10} style={{ padding: 8 }}>
2319
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2320
+ </Pressable>)}
2321
+ </View>
2322
+ </View>
2491
2323
 
2492
- <View style={styles.detailSearchRow}>
2493
- <View style={styles.detailSearchBox}>
2494
- <TextInput placeholder="Search in request / response..." placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={styles.detailSearchInput} autoCorrect={false} autoCapitalize="none"/>
2495
- {detailSearch.length > 0 && (<Pressable onPress={() => setDetailSearch('')} hitSlop={10} style={{ padding: 8 }}>
2496
- <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2497
- </Pressable>)}
2498
- </View>
2499
- <TouchableScale style={[styles.iconSquareBtn, { marginLeft: 8 }]} onPress={() => {
2500
- const next = reqExpanded === true || resExpanded === true
2501
- ? false
2502
- : true;
2503
- setReqExpanded(next);
2504
- setResExpanded(next);
2505
- }} hitSlop={10}>
2506
- <ExpandCollapseIcon isExpanded={reqExpanded === true || resExpanded === true} color={reqExpanded === true || resExpanded === true
2507
- ? AppColors.purple
2508
- : AppColors.grayTextWeak} size={14}/>
2509
- </TouchableScale>
2510
- </View>
2324
+ <HeadersSection title="Request Headers" headers={selected.requestHeaders} search={detailSearch} resetKey={selected.id}/>
2325
+ <HeadersSection title="Response Headers" headers={selected.responseHeaders} search={detailSearch} resetKey={selected.id}/>
2326
+ </>)}
2511
2327
 
2512
- <HeadersSection title="Request Headers" headers={selected.requestHeaders} search={detailSearch} resetKey={selected.id}/>
2513
- <HeadersSection title="Response Headers" headers={selected.responseHeaders} search={detailSearch} resetKey={selected.id}/>
2328
+ {apiDetailActiveTab === 'request' && selected.request != null && (<>
2329
+ <View style={styles.detailSearchRow}>
2330
+ <View style={styles.detailSearchBox}>
2331
+ <TextInput placeholder="Search request..." placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={styles.detailSearchInput} autoCorrect={false} autoCapitalize="none"/>
2332
+ {detailSearch.length > 0 && (<Pressable onPress={() => setDetailSearch('')} hitSlop={10} style={{ padding: 8 }}>
2333
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2334
+ </Pressable>)}
2335
+ </View>
2336
+ </View>
2514
2337
 
2515
- {selected.request != null && selected.method !== 'GET' && (<View style={styles.sectionContainer}>
2516
- <SectionHeader title="Request" value={selected.request} expanded={reqExpanded} onToggleExpand={() => setReqExpanded(v => !v)} showDiff={prevRequestData != null} isDiffing={showReqDiff} onToggleDiff={() => {
2338
+ <View style={styles.sectionContainer}>
2339
+ <SectionHeader title="Request" value={selected.request} expanded={reqExpanded} onToggleExpand={() => setReqExpanded(v => !v)} showDiff={prevRequestData != null} isDiffing={showReqDiff} onToggleDiff={() => {
2517
2340
  setShowReqDiff(v => !v);
2518
2341
  if (!reqExpanded && !showReqDiff)
2519
2342
  setReqExpanded(true);
2520
2343
  }}/>
2521
- {showReqDiff ? (<DiffViewer oldData={prevRequestData} newData={selected.request} forceOpen={reqExpanded}/>) : (<JsonViewer data={selected.request} search={detailSearch} forceOpen={reqExpanded}/>)}
2522
- </View>)}
2344
+ {showReqDiff ? (<DiffViewer oldData={prevRequestData} newData={selected.request} forceOpen={reqExpanded}/>) : (<JsonViewer data={selected.request} search={detailSearch} forceOpen={reqExpanded}/>)}
2345
+ </View>
2346
+ </>)}
2523
2347
 
2524
- <View style={styles.sectionContainer}>
2525
- <SectionHeader title="Response" value={selected.response} expanded={resExpanded} onToggleExpand={() => setResExpanded(v => !v)} showDiff={prevResponseData != null} isDiffing={showResDiff} onToggleDiff={() => {
2526
- setShowResDiff(v => !v);
2527
- if (!resExpanded && !showResDiff)
2528
- setResExpanded(true);
2529
- }}/>
2530
- {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded}/>)}
2531
- </View>
2532
- </ScrollView>)) : (<View style={styles.empty}>
2348
+ {apiDetailActiveTab === 'response' && (<>
2349
+ <View style={styles.detailSearchRow}>
2350
+ <View style={styles.detailSearchBox}>
2351
+ <TextInput placeholder="Search response..." placeholderTextColor={AppColors.grayTextWeak} value={detailSearch} onChangeText={setDetailSearch} style={styles.detailSearchInput} autoCorrect={false} autoCapitalize="none"/>
2352
+ {detailSearch.length > 0 && (<Pressable onPress={() => setDetailSearch('')} hitSlop={10} style={{ padding: 8 }}>
2353
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
2354
+ </Pressable>)}
2355
+ </View>
2356
+ </View>
2357
+
2358
+ <View style={styles.sectionContainer}>
2359
+ <SectionHeader title="Response" value={selected.response} expanded={resExpanded} onToggleExpand={() => setResExpanded(v => !v)} showDiff={prevResponseData != null} isDiffing={showResDiff} onToggleDiff={() => {
2360
+ setShowResDiff(v => !v);
2361
+ if (!resExpanded && !showResDiff)
2362
+ setResExpanded(true);
2363
+ }}/>
2364
+ {showResDiff ? (<DiffViewer oldData={prevResponseData} newData={selected.response} forceOpen={resExpanded}/>) : (<JsonViewer data={selected.response} search={detailSearch} forceOpen={resExpanded}/>)}
2365
+ </View>
2366
+ </>)}
2367
+ </ScrollView>
2368
+ </View>)) : (<View style={styles.empty}>
2533
2369
  <ActivityIndicator size="large" color={AppColors.purple}/>
2534
2370
  <Text style={[styles.emptySub, { marginTop: 12 }]}>
2535
2371
  Loading logs...
2536
2372
  </Text>
2537
2373
  </View>)}
2538
- </View>)}
2374
+ </View>
2375
+ </View>
2376
+ </ErrorBoundary>)}
2539
2377
  </Modal>
2540
2378
  </>);
2541
2379
  };
2542
- export default NetworkInspector;
2543
- // ─── Analytics list styles (Firebase DebugView look) ─────────────────────────
2544
- const analyticsListStyles = StyleSheet.create({
2545
- cardRow: {},
2546
- // ── Top Events summary card (mirrors Firebase DebugView right panel) ────────
2547
- topEventsCard: {
2548
- marginHorizontal: 16,
2549
- marginBottom: 12,
2550
- backgroundColor: AppColors.primaryLight,
2551
- borderRadius: 12,
2552
- borderWidth: 1,
2553
- borderColor: AppColors.grayBorderSecondary,
2554
- paddingHorizontal: 16,
2555
- paddingVertical: 12,
2556
- shadowColor: '#000',
2557
- shadowOpacity: 0.04,
2558
- shadowRadius: 4,
2559
- shadowOffset: { width: 0, height: 1 },
2560
- elevation: 1,
2561
- },
2562
- topEventsHeaderRow: {
2563
- flexDirection: 'row',
2564
- alignItems: 'center',
2565
- justifyContent: 'space-between',
2566
- marginBottom: 12,
2567
- },
2568
- topEventsTitle: {
2569
- fontFamily: AppFonts.interBold,
2570
- fontSize: 10,
2571
- color: AppColors.grayTextWeak,
2572
- letterSpacing: 0.8,
2573
- textTransform: 'uppercase',
2574
- },
2575
- topEventsSubtitle: {
2576
- fontFamily: AppFonts.interMedium,
2577
- fontSize: 9,
2578
- color: AppColors.grayTextWeak,
2579
- letterSpacing: 0.4,
2580
- },
2581
- topEventRow: {
2582
- flexDirection: 'row',
2583
- alignItems: 'center',
2584
- marginBottom: 8,
2585
- gap: 8,
2586
- },
2587
- topEventName: {
2588
- fontFamily: AppFonts.interMedium,
2589
- fontSize: 12,
2590
- color: AppColors.primaryBlack,
2591
- flex: 1,
2592
- },
2593
- topEventBarWrap: {
2594
- flex: 0.8,
2595
- height: 3,
2596
- backgroundColor: AppColors.grayBackground,
2597
- borderRadius: 2,
2598
- overflow: 'hidden',
2599
- },
2600
- topEventBar: {
2601
- height: '100%',
2602
- borderRadius: 2,
2603
- },
2604
- topEventCount: {
2605
- fontFamily: AppFonts.interBold,
2606
- fontSize: 11,
2607
- color: AppColors.grayText,
2608
- width: 24,
2609
- textAlign: 'right',
2610
- },
2611
- iconCircle: {
2612
- width: 24,
2613
- height: 24,
2614
- borderRadius: 12,
2615
- alignItems: 'center',
2616
- justifyContent: 'center',
2617
- },
2618
- });
2380
+ const NetworkInspectorWrapper = (props) => {
2381
+ return (<ErrorBoundary fallbackType="inline">
2382
+ <NetworkInspector {...props}/>
2383
+ </ErrorBoundary>);
2384
+ };
2385
+ export default NetworkInspectorWrapper;
2619
2386
  // Re-export public APIs
2620
2387
  export { setupNetworkLogger, clearNetworkLogs, subscribeNetworkLogs, } from './customHooks/networkLogger';
2621
2388
  export { setupConsoleLogger, clearConsoleLogs, subscribeConsoleLogs, } from './customHooks/consoleLogger';
2622
2389
  export { setupAnalyticsLogger, logAnalyticsEvent, subscribeAnalyticsEvents, clearAnalyticsEvents, } from './customHooks/analyticsLogger';
2623
- export { getWebViewLogs, getWebViewNavHistory, getWebViewHtml, getWebViewCss, getWebViewJs, getWebViewHtmlUrl, clearWebViewData, subscribeWebView, } from './customHooks/webViewLogger';
2390
+ export { WebView, getWebViewLogs, getWebViewNavHistory, getWebViewHtml, getWebViewCss, getWebViewJs, getWebViewHtmlUrl, clearWebViewData, subscribeWebView, } from './customHooks/webViewLogger';
2391
+ export { default as ErrorBoundary } from './components/ErrorBoundary';
2392
+ export { connectReduxStore, getReduxState, subscribeReduxState, } from './customHooks/reduxLogger';