react-native-inapp-inspector 2.3.0 → 2.3.2

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.
@@ -68,7 +68,7 @@ const NativeInspector_2 = require("./native/NativeInspector");
68
68
  const constants_1 = require("./constants");
69
69
  // Stylesheet
70
70
  const styles_1 = require("./styles");
71
- const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigationRef, appIcon, environment, initialVisible, visible: controlledVisible, }) => {
71
+ const NetworkInspector = ({ enabled = true, storage, navigationRef, appIcon, environment, initialVisible = false, visible: controlledVisible, }) => {
72
72
  // Set custom storage synchronously during render phase
73
73
  (0, settingsStore_1.setCustomStorage)(storage || null);
74
74
  const [isDark, setIsDark] = (0, react_1.useState)(false);
@@ -80,7 +80,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
80
80
  const [modalHeightPercent, setModalHeightPercent] = (0, react_1.useState)(90);
81
81
  const [modalAnimationType, setModalAnimationType] = (0, react_1.useState)('slide');
82
82
  const [logs, setLogs] = (0, react_1.useState)([]);
83
- const [visible, setVisible] = (0, react_1.useState)(initialVisible ?? controlledVisible ?? false);
83
+ const [visible, setVisible] = (0, react_1.useState)(controlledVisible ?? initialVisible ?? false);
84
84
  (0, react_1.useEffect)(() => {
85
85
  if (controlledVisible !== undefined) {
86
86
  setVisible(controlledVisible);
@@ -784,22 +784,26 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
784
784
  const useNativeFab = (react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android') && isNativeModule;
785
785
  // 100% Native Main-Thread Floating Button Lifecycle
786
786
  (0, react_1.useEffect)(() => {
787
- if (!useNativeFab || !isEnabled || !enabled) {
787
+ if (!useNativeFab || !enabled) {
788
788
  if (useNativeFab) {
789
- (0, NativeInspector_2.hideNativeFloatingButton)();
789
+ (0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
790
790
  }
791
791
  return;
792
792
  }
793
793
  if (visible) {
794
- (0, NativeInspector_2.hideNativeFloatingButton)();
794
+ (0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
795
795
  }
796
796
  else {
797
- (0, NativeInspector_2.showNativeFloatingButton)();
798
- (0, NativeInspector_2.setNativeFloatingButtonBadge)(logs.length > 0 || analyticsEvents.length > 0);
797
+ (0, NativeInspector_2.showNativeFloatingButton)().catch(() => { });
798
+ (0, NativeInspector_2.setNativeFloatingButtonBadge)(logs.length > 0 || analyticsEvents.length > 0).catch(() => { });
799
799
  }
800
+ return () => {
801
+ if (useNativeFab) {
802
+ (0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
803
+ }
804
+ };
800
805
  }, [
801
806
  useNativeFab,
802
- isEnabled,
803
807
  enabled,
804
808
  visible,
805
809
  logs.length,
@@ -858,20 +862,11 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
858
862
  if (freshCrashes.length > 0) {
859
863
  setCrashRecords(freshCrashes);
860
864
  }
861
- }
862
- // eslint-disable-next-line react-hooks/exhaustive-deps
863
- }, [visible]);
864
- (0, react_1.useEffect)(() => {
865
- if (visible) {
866
- const task = react_native_1.InteractionManager.runAfterInteractions(() => {
865
+ const frame = requestAnimationFrame(() => {
867
866
  setIsReady(true);
868
867
  });
869
- const fallbackTimer = setTimeout(() => {
870
- setIsReady(true);
871
- }, 350);
872
868
  return () => {
873
- task.cancel();
874
- clearTimeout(fallbackTimer);
869
+ cancelAnimationFrame(frame);
875
870
  };
876
871
  }
877
872
  else {
@@ -1707,7 +1702,8 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
1707
1702
  setVisible,
1708
1703
  closeModal,
1709
1704
  isReady,
1710
- isEnabled,
1705
+ enabled,
1706
+ isEnabled: enabled,
1711
1707
  appIcon,
1712
1708
  environment,
1713
1709
  modalHeightPercent,
@@ -1884,14 +1880,22 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
1884
1880
  </InspectorContext_1.InspectorContext.Provider>);
1885
1881
  };
1886
1882
  const NetworkInspectorWrapper = (props) => {
1887
- // If running in production release build and not explicitly force-enabled (e.g. for QA builds),
1888
- // return null immediately for 0-overhead production stubbing.
1889
- if (typeof __DEV__ !== 'undefined' && !__DEV__ && !props?.forceEnable) {
1883
+ const enabled = props?.enabled ?? true;
1884
+ (0, react_1.useEffect)(() => {
1885
+ if (!enabled) {
1886
+ (0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
1887
+ }
1888
+ return () => {
1889
+ (0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
1890
+ };
1891
+ }, [enabled]);
1892
+ // If enabled is false, return null immediately with 0 overhead
1893
+ if (!enabled) {
1890
1894
  return null;
1891
1895
  }
1892
1896
  return (<i18n_1.I18nextProvider i18n={i18n_1.i18n}>
1893
1897
  <ErrorBoundary_1.default fallbackType="inline">
1894
- <NetworkInspector {...props}/>
1898
+ <NetworkInspector {...props} enabled={enabled}/>
1895
1899
  </ErrorBoundary_1.default>
1896
1900
  </i18n_1.I18nextProvider>);
1897
1901
  };
@@ -157,8 +157,6 @@ export interface PersistedSettings {
157
157
  }
158
158
  export interface NetworkInspectorProps {
159
159
  enabled?: boolean;
160
- isEnabled?: boolean;
161
- forceEnable?: boolean;
162
160
  storage?: InspectorStorage;
163
161
  navigationRef?: any;
164
162
  appIcon?: any;
@@ -174,6 +172,7 @@ export interface InspectorContextValue {
174
172
  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
175
173
  closeModal: () => void;
176
174
  isReady: boolean;
175
+ enabled: boolean;
177
176
  isEnabled: boolean;
178
177
  appIcon?: any;
179
178
  environment?: string;
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
- import { ActivityIndicator, Animated, Modal, Platform, Pressable, StatusBar, StyleSheet, Text, View, } from 'react-native';
2
+ import { Animated, Modal, Platform, Pressable, StatusBar, StyleSheet, View, } from 'react-native';
3
3
  import { useInspector } from './InspectorContext';
4
4
  import ErrorBoundary from '../ErrorBoundary';
5
5
  import FabLauncher from './FabLauncher';
@@ -11,6 +11,7 @@ import LogDetail from './LogDetail';
11
11
  import ConsoleTab from './ConsoleTab';
12
12
  import AnalyticsTab from './AnalyticsTab';
13
13
  import AnalyticsDetail from '../AnalyticsDetail';
14
+ import SkeletonPlaceholder from '../SkeletonPlaceholder';
14
15
  import ReduxTab from './ReduxTab';
15
16
  import ReduxDetail from './ReduxDetail';
16
17
  import BundleTab from './BundleTab';
@@ -26,7 +27,7 @@ import styles from '../../styles';
26
27
  import { AppColors } from '../../styles/AppColors';
27
28
  import NavigationTracker from './NavigationTracker';
28
29
  const MainScreen = () => {
29
- const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, isEnabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
30
+ const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, enabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
30
31
  const isDetailActive = (activeTab === 'apis' && selected != null) ||
31
32
  (activeTab === 'analytics' && selectedEvent != null) ||
32
33
  (activeTab === 'logs' && selectedLog != null) ||
@@ -68,7 +69,7 @@ const MainScreen = () => {
68
69
  }, [activeTab]);
69
70
  return (<>
70
71
  {(Platform.OS === 'ios' || Platform.OS === 'android') &&
71
- isEnabled &&
72
+ enabled &&
72
73
  !visible &&
73
74
  !useNativeFab && <FabLauncher />}
74
75
  <Modal visible={visible} animationType={modalAnimationType} transparent statusBarTranslucent={true}>
@@ -88,10 +89,10 @@ const MainScreen = () => {
88
89
  <InspectorHeader />
89
90
 
90
91
  <View style={{ flex: 1 }}>
91
- {isReady ? (<View style={{ flex: 1 }}>
92
- {/* ─── Horizontal Scrollable Tab Bar inside Content ─── */}
93
- {!isDetailActive && <TabBar />}
92
+ {/* ─── Horizontal Scrollable Tab Bar inside Content (Always visible) ─── */}
93
+ {!isDetailActive && <TabBar />}
94
94
 
95
+ {isReady ? (<View style={{ flex: 1 }}>
95
96
  {/* Persistent List Layer - Never unmounted, preserves 100% native scroll with smooth tab transition */}
96
97
  <Animated.View style={[
97
98
  {
@@ -143,12 +144,7 @@ const MainScreen = () => {
143
144
  {activeTab === 'redux' && <ReduxDetail />}
144
145
  {activeTab === 'crash' && selectedCrash != null && (<CrashDetail />)}
145
146
  </Animated.View>)}
146
- </View>) : (<View style={styles.empty}>
147
- <ActivityIndicator size="large" color={AppColors.purple}/>
148
- <Text style={[styles.emptySub, { marginTop: 12 }]}>
149
- Loading logs...
150
- </Text>
151
- </View>)}
147
+ </View>) : (<SkeletonPlaceholder />)}
152
148
 
153
149
  {/* Settings Panel Layer - Rendered on top with smooth slide & spring transition */}
154
150
  {settingsPage !== null && (<Animated.View style={[
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect, useMemo, useCallback } from 'react';
2
- import { View, Text, ScrollView, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
2
+ import { View, Text, ScrollView, FlatList, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
3
3
  import TouchableScale from '../TouchableScale';
4
4
  import { AppColors } from '../../styles/AppColors';
5
5
  import { AppFonts } from '../../styles/AppFonts';
@@ -8,6 +8,92 @@ import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, C
8
8
  import { fetchStorageEntries, setStorageEntry, removeStorageEntry, clearStorageDriver, isAsyncStorageConnected, isMMKVConnected, getRegisteredMMKVInstanceIds, subscribeToStorageChanges, } from '../../customHooks/storageInspector';
9
9
  import { copyToClipboard } from '../../helpers';
10
10
  import { showToast } from '../../helpers/toast';
11
+ const getTypeBadge = (type) => {
12
+ switch (type) {
13
+ case 'json':
14
+ return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
15
+ case 'number':
16
+ return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
17
+ case 'boolean':
18
+ return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
19
+ case 'null':
20
+ return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
21
+ default:
22
+ return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
23
+ }
24
+ };
25
+ const StorageEntryCard = React.memo(function StorageEntryCard({ entry, isExpanded, onToggleExpand, onCopy, onEdit, onDelete, badge, }) {
26
+ const formattedBytes = useMemo(() => {
27
+ return entry.byteSize < 1024
28
+ ? `${entry.byteSize} B`
29
+ : `${(entry.byteSize / 1024).toFixed(1)} KB`;
30
+ }, [entry.byteSize]);
31
+ // Lazy compute displayed value to avoid layout thrashing on large JSON strings
32
+ const displayValue = useMemo(() => {
33
+ if (!isExpanded) {
34
+ if (entry.value.length > 250) {
35
+ return entry.value.slice(0, 250) + '...';
36
+ }
37
+ return entry.value;
38
+ }
39
+ if (entry.type === 'json') {
40
+ try {
41
+ const parsed = entry.parsedValue ?? JSON.parse(entry.value);
42
+ return JSON.stringify(parsed, null, 2);
43
+ }
44
+ catch {
45
+ return entry.value;
46
+ }
47
+ }
48
+ return entry.value;
49
+ }, [isExpanded, entry.value, entry.type, entry.parsedValue]);
50
+ return (<View style={styles.entryCard}>
51
+ {/* Entry Header: Key name, Type Badge, Size, Actions */}
52
+ <View style={styles.entryHeader}>
53
+ <View style={{ flex: 1, marginRight: 8 }}>
54
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
55
+ <Text style={styles.entryKey} numberOfLines={1} selectable>
56
+ {entry.key}
57
+ </Text>
58
+ <View style={[
59
+ styles.typeBadge,
60
+ { backgroundColor: badge.bg, borderColor: `${badge.color}33` },
61
+ ]}>
62
+ <Text style={[styles.typeBadgeText, { color: badge.color }]}>
63
+ {badge.label}
64
+ </Text>
65
+ </View>
66
+ <Text style={styles.sizeText}>{formattedBytes}</Text>
67
+ </View>
68
+ </View>
69
+
70
+ {/* Actions: Copy, Edit, Delete */}
71
+ <View style={styles.entryActions}>
72
+ <TouchableScale hitSlop={6} onPress={() => onCopy(entry)} style={styles.entryActionBtn}>
73
+ <CopyIcon size={12} color={AppColors.grayText}/>
74
+ </TouchableScale>
75
+
76
+ <TouchableScale hitSlop={6} onPress={() => onEdit(entry)} style={styles.entryActionBtn}>
77
+ <PencilIcon size={12} color={AppColors.purple}/>
78
+ </TouchableScale>
79
+
80
+ <TouchableScale hitSlop={6} onPress={() => onDelete(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
81
+ <TrashIcon size={12} color={AppColors.errorColor}/>
82
+ </TouchableScale>
83
+ </View>
84
+ </View>
85
+
86
+ {/* Entry Value Preview / Viewer */}
87
+ <TouchableScale onPress={() => onToggleExpand(entry.key)} style={styles.valuePreviewBox}>
88
+ <Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable={isExpanded}>
89
+ {displayValue}
90
+ </Text>
91
+ {entry.value.length > 80 && (<Text style={styles.expandHint}>
92
+ {isExpanded ? '▲ Collapse' : '▼ Expand'}
93
+ </Text>)}
94
+ </TouchableScale>
95
+ </View>);
96
+ });
11
97
  export const StorageTab = React.memo(() => {
12
98
  const { t } = useTranslation();
13
99
  const [activeDriver, setActiveDriver] = useState('asyncStorage');
@@ -166,20 +252,11 @@ export const StorageTab = React.memo(() => {
166
252
  return `${(totalBytes / 1024).toFixed(1)} KB`;
167
253
  return `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`;
168
254
  }, [totalBytes]);
169
- const getTypeBadge = (type) => {
170
- switch (type) {
171
- case 'json':
172
- return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
173
- case 'number':
174
- return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
175
- case 'boolean':
176
- return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
177
- case 'null':
178
- return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
179
- default:
180
- return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
181
- }
182
- };
255
+ const handleCopyEntry = useCallback((entry) => {
256
+ copyToClipboard(entry.value, entry.key);
257
+ showToast(`Copied value of "${entry.key}"`);
258
+ }, []);
259
+ const renderItem = useCallback(({ item }) => (<StorageEntryCard entry={item} isExpanded={Boolean(expandedKeys[item.key])} onToggleExpand={toggleExpand} onCopy={handleCopyEntry} onEdit={handleOpenEdit} onDelete={handleDeleteKey} badge={getTypeBadge(item.type)}/>), [expandedKeys, handleCopyEntry]);
183
260
  const isConnected = activeDriver === 'asyncStorage'
184
261
  ? isAsyncStorageConnected()
185
262
  : isMMKVConnected();
@@ -295,81 +372,19 @@ export const StorageTab = React.memo(() => {
295
372
  {isLoading ? (<View style={styles.loadingContainer}>
296
373
  <ActivityIndicator size="small" color={AppColors.purple}/>
297
374
  <Text style={styles.loadingText}>Loading storage entries...</Text>
298
- </View>) : filteredEntries.length === 0 ? (<ScrollView contentContainerStyle={styles.emptyContainer} showsVerticalScrollIndicator={false}>
299
- <View style={styles.emptyIconWrap}>
300
- <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
301
- </View>
302
- <Text style={styles.emptyTitle}>
303
- {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
304
- </Text>
305
- <Text style={styles.emptySubtitle}>
306
- {search
307
- ? 'Try modifying your search query'
308
- : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
309
- </Text>
310
- </ScrollView>) : (<ScrollView style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
311
- {filteredEntries.map(entry => {
312
- const isExpanded = Boolean(expandedKeys[entry.key]);
313
- const badge = getTypeBadge(entry.type);
314
- const formattedBytes = entry.byteSize < 1024
315
- ? `${entry.byteSize} B`
316
- : `${(entry.byteSize / 1024).toFixed(1)} KB`;
317
- return (<View key={entry.key} style={styles.entryCard}>
318
- {/* Entry Header: Key name, Type Badge, Size, Actions */}
319
- <View style={styles.entryHeader}>
320
- <View style={{ flex: 1, marginRight: 8 }}>
321
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
322
- <Text style={styles.entryKey} numberOfLines={1} selectable>
323
- {entry.key}
324
- </Text>
325
- <View style={[
326
- styles.typeBadge,
327
- { backgroundColor: badge.bg, borderColor: `${badge.color}33` },
328
- ]}>
329
- <Text style={[styles.typeBadgeText, { color: badge.color }]}>
330
- {badge.label}
331
- </Text>
332
- </View>
333
- <Text style={styles.sizeText}>{formattedBytes}</Text>
334
- </View>
335
- </View>
336
-
337
- {/* Actions: Copy, Edit, Delete */}
338
- <View style={styles.entryActions}>
339
- <TouchableScale hitSlop={6} onPress={() => {
340
- copyToClipboard(entry.value, entry.key);
341
- showToast(`Copied value of "${entry.key}"`);
342
- }} style={styles.entryActionBtn}>
343
- <CopyIcon size={12} color={AppColors.grayText}/>
344
- </TouchableScale>
345
-
346
- <TouchableScale hitSlop={6} onPress={() => handleOpenEdit(entry)} style={styles.entryActionBtn}>
347
- <PencilIcon size={12} color={AppColors.purple}/>
348
- </TouchableScale>
349
-
350
- <TouchableScale hitSlop={6} onPress={() => handleDeleteKey(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
351
- <TrashIcon size={12} color={AppColors.errorColor}/>
352
- </TouchableScale>
353
- </View>
354
- </View>
355
-
356
- {/* Entry Value Preview / Viewer */}
357
- <TouchableScale onPress={() => toggleExpand(entry.key)} style={styles.valuePreviewBox}>
358
- <Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable>
359
- {entry.type === 'json' && entry.parsedValue
360
- ? isExpanded
361
- ? JSON.stringify(entry.parsedValue, null, 2)
362
- : entry.value
363
- : entry.value}
364
- </Text>
365
- {entry.value.length > 80 && (<Text style={styles.expandHint}>
366
- {isExpanded ? '▲ Collapse' : '▼ Expand'}
367
- </Text>)}
368
- </TouchableScale>
369
- </View>);
370
- })}
371
- <View style={{ height: 60 }}/>
372
- </ScrollView>)}
375
+ </View>) : (<FlatList data={filteredEntries} keyExtractor={item => item.key} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={Platform.OS === 'android'} style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} ListEmptyComponent={<View style={styles.emptyContainer}>
376
+ <View style={styles.emptyIconWrap}>
377
+ <DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
378
+ </View>
379
+ <Text style={styles.emptyTitle}>
380
+ {search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
381
+ </Text>
382
+ <Text style={styles.emptySubtitle}>
383
+ {search
384
+ ? 'Try modifying your search query'
385
+ : `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
386
+ </Text>
387
+ </View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>)}
373
388
 
374
389
  {/* ── Create / Edit Key Modal ── */}
375
390
  <Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface SkeletonPlaceholderProps {
3
+ cardCount?: number;
4
+ }
5
+ declare const SkeletonPlaceholder: React.NamedExoticComponent<SkeletonPlaceholderProps>;
6
+ export default SkeletonPlaceholder;
@@ -0,0 +1,173 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, StyleSheet, View } from 'react-native';
3
+ import { AppColors } from '../styles/AppColors';
4
+ const SkeletonPlaceholder = React.memo(function SkeletonPlaceholder({ cardCount = 4, }) {
5
+ const shimmerAnim = useRef(new Animated.Value(0.35)).current;
6
+ useEffect(() => {
7
+ const animation = Animated.loop(Animated.sequence([
8
+ Animated.timing(shimmerAnim, {
9
+ toValue: 0.85,
10
+ duration: 750,
11
+ useNativeDriver: true,
12
+ }),
13
+ Animated.timing(shimmerAnim, {
14
+ toValue: 0.35,
15
+ duration: 750,
16
+ useNativeDriver: true,
17
+ }),
18
+ ]));
19
+ animation.start();
20
+ return () => animation.stop();
21
+ }, [shimmerAnim]);
22
+ return (<View style={skeletonStyles.container}>
23
+ {/* ─── Search & Scope Toolbar Skeleton ─── */}
24
+ <View style={skeletonStyles.toolbarSkeleton}>
25
+ <Animated.View style={[
26
+ skeletonStyles.searchBarSkeleton,
27
+ { opacity: shimmerAnim },
28
+ ]}/>
29
+ <View style={skeletonStyles.actionButtonsRow}>
30
+ <Animated.View style={[skeletonStyles.iconButtonSkeleton, { opacity: shimmerAnim }]}/>
31
+ <Animated.View style={[skeletonStyles.iconButtonSkeleton, { opacity: shimmerAnim }]}/>
32
+ </View>
33
+ </View>
34
+
35
+ {/* ─── Quick Filter Chips Skeleton Strip ─── */}
36
+ <View style={skeletonStyles.chipStripSkeleton}>
37
+ <Animated.View style={[skeletonStyles.chipSkeleton, { width: 48, opacity: shimmerAnim }]}/>
38
+ <Animated.View style={[skeletonStyles.chipSkeleton, { width: 68, opacity: shimmerAnim }]}/>
39
+ <Animated.View style={[skeletonStyles.chipSkeleton, { width: 76, opacity: shimmerAnim }]}/>
40
+ <Animated.View style={[skeletonStyles.chipSkeleton, { width: 58, opacity: shimmerAnim }]}/>
41
+ </View>
42
+
43
+ {/* ─── List Cards Skeleton ─── */}
44
+ {Array.from({ length: cardCount }).map((_, i) => (<Animated.View key={`skeleton_card_${i}`} style={[skeletonStyles.cardSkeleton, { opacity: shimmerAnim }]}>
45
+ {/* Top row: Status pill + Method + Time */}
46
+ <View style={skeletonStyles.cardTopRow}>
47
+ <View style={skeletonStyles.badgeGroup}>
48
+ <View style={skeletonStyles.statusBadgeSkeleton}/>
49
+ <View style={skeletonStyles.methodBadgeSkeleton}/>
50
+ </View>
51
+ <View style={skeletonStyles.timeSkeleton}/>
52
+ </View>
53
+
54
+ {/* Middle row: URL lines */}
55
+ <View style={skeletonStyles.urlLineLong}/>
56
+ <View style={skeletonStyles.urlLineShort}/>
57
+
58
+ {/* Bottom row: Latency & Size */}
59
+ <View style={skeletonStyles.cardBottomRow}>
60
+ <View style={skeletonStyles.metaPillSkeleton}/>
61
+ <View style={skeletonStyles.metaPillSkeleton}/>
62
+ </View>
63
+ </Animated.View>))}
64
+ </View>);
65
+ });
66
+ const skeletonStyles = StyleSheet.create({
67
+ container: {
68
+ flex: 1,
69
+ paddingHorizontal: 12,
70
+ paddingTop: 8,
71
+ },
72
+ toolbarSkeleton: {
73
+ flexDirection: 'row',
74
+ alignItems: 'center',
75
+ gap: 8,
76
+ marginBottom: 8,
77
+ },
78
+ searchBarSkeleton: {
79
+ flex: 1,
80
+ height: 36,
81
+ borderRadius: 8,
82
+ backgroundColor: AppColors.graySurface,
83
+ borderWidth: 1,
84
+ borderColor: AppColors.dividerColor,
85
+ },
86
+ actionButtonsRow: {
87
+ flexDirection: 'row',
88
+ gap: 6,
89
+ },
90
+ iconButtonSkeleton: {
91
+ width: 36,
92
+ height: 36,
93
+ borderRadius: 8,
94
+ backgroundColor: AppColors.graySurface,
95
+ borderWidth: 1,
96
+ borderColor: AppColors.dividerColor,
97
+ },
98
+ chipStripSkeleton: {
99
+ flexDirection: 'row',
100
+ gap: 6,
101
+ marginBottom: 10,
102
+ },
103
+ chipSkeleton: {
104
+ height: 24,
105
+ borderRadius: 6,
106
+ backgroundColor: AppColors.graySurface,
107
+ borderWidth: 1,
108
+ borderColor: AppColors.dividerColor,
109
+ },
110
+ cardSkeleton: {
111
+ backgroundColor: AppColors.primaryLight,
112
+ borderRadius: 10,
113
+ padding: 12,
114
+ marginBottom: 8,
115
+ borderWidth: 1,
116
+ borderColor: AppColors.dividerColor,
117
+ },
118
+ cardTopRow: {
119
+ flexDirection: 'row',
120
+ justifyContent: 'space-between',
121
+ alignItems: 'center',
122
+ marginBottom: 8,
123
+ },
124
+ badgeGroup: {
125
+ flexDirection: 'row',
126
+ alignItems: 'center',
127
+ gap: 6,
128
+ },
129
+ statusBadgeSkeleton: {
130
+ width: 38,
131
+ height: 18,
132
+ borderRadius: 4,
133
+ backgroundColor: AppColors.graySurface,
134
+ },
135
+ methodBadgeSkeleton: {
136
+ width: 44,
137
+ height: 18,
138
+ borderRadius: 4,
139
+ backgroundColor: AppColors.graySurface,
140
+ },
141
+ timeSkeleton: {
142
+ width: 48,
143
+ height: 12,
144
+ borderRadius: 4,
145
+ backgroundColor: AppColors.graySurface,
146
+ },
147
+ urlLineLong: {
148
+ height: 13,
149
+ borderRadius: 4,
150
+ backgroundColor: AppColors.graySurface,
151
+ marginBottom: 5,
152
+ width: '90%',
153
+ },
154
+ urlLineShort: {
155
+ height: 11,
156
+ borderRadius: 4,
157
+ backgroundColor: AppColors.graySurface,
158
+ marginBottom: 8,
159
+ width: '55%',
160
+ },
161
+ cardBottomRow: {
162
+ flexDirection: 'row',
163
+ gap: 8,
164
+ marginTop: 2,
165
+ },
166
+ metaPillSkeleton: {
167
+ width: 52,
168
+ height: 14,
169
+ borderRadius: 4,
170
+ backgroundColor: AppColors.graySurface,
171
+ },
172
+ });
173
+ export default SkeletonPlaceholder;
@@ -1 +1 @@
1
- export declare const LIB_VERSION = "2.3.0";
1
+ export declare const LIB_VERSION = "2.3.2";
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED FILE — do not edit by hand.
2
2
  // Regenerated from package.json on every build by scripts/gen-version.js.
3
- export const LIB_VERSION = '2.3.0';
3
+ export const LIB_VERSION = '2.3.2';
@@ -37,11 +37,11 @@ export declare const getRegisteredMMKVInstanceIds: () => string[];
37
37
  */
38
38
  export declare const calculateByteSize: (str: string) => number;
39
39
  /**
40
- * Determine type and format value safely
40
+ * Determine type and format value safely without blocking JS thread
41
41
  */
42
42
  export declare const analyzeStorageValue: (rawVal: any) => {
43
43
  strVal: string;
44
- parsedVal?: any;
44
+ parsedValue?: any;
45
45
  type: "json" | "string" | "number" | "boolean" | "null";
46
46
  byteSize: number;
47
47
  };