react-native-inapp-inspector 1.1.12 → 1.1.13

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 (45) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +64 -21
  2. package/dist/commonjs/components/HighlightText.js +1 -1
  3. package/dist/commonjs/components/JsonViewer.d.ts +3 -1
  4. package/dist/commonjs/components/JsonViewer.js +327 -9
  5. package/dist/commonjs/components/MetaAccordion.d.ts +10 -1
  6. package/dist/commonjs/components/MetaAccordion.js +121 -24
  7. package/dist/commonjs/components/NetworkIcons.d.ts +1 -0
  8. package/dist/commonjs/components/NetworkIcons.js +8 -1
  9. package/dist/commonjs/components/ReduxTreeView.d.ts +12 -3
  10. package/dist/commonjs/components/ReduxTreeView.js +1096 -278
  11. package/dist/commonjs/components/SectionHeader.d.ts +1 -1
  12. package/dist/commonjs/components/SectionHeader.js +7 -1
  13. package/dist/commonjs/constants/version.d.ts +1 -1
  14. package/dist/commonjs/constants/version.js +1 -1
  15. package/dist/commonjs/customHooks/reduxLogger.d.ts +2 -0
  16. package/dist/commonjs/customHooks/reduxLogger.js +2 -0
  17. package/dist/commonjs/helpers/index.d.ts +1 -0
  18. package/dist/commonjs/helpers/index.js +15 -1
  19. package/dist/commonjs/helpers/settingsStore.d.ts +1 -0
  20. package/dist/commonjs/helpers/settingsStore.js +4 -0
  21. package/dist/commonjs/index.js +110 -91
  22. package/dist/commonjs/types/index.d.ts +3 -0
  23. package/dist/esm/components/AnalyticsDetail.js +64 -21
  24. package/dist/esm/components/HighlightText.js +3 -3
  25. package/dist/esm/components/JsonViewer.d.ts +3 -1
  26. package/dist/esm/components/JsonViewer.js +295 -10
  27. package/dist/esm/components/MetaAccordion.d.ts +10 -1
  28. package/dist/esm/components/MetaAccordion.js +90 -26
  29. package/dist/esm/components/NetworkIcons.d.ts +1 -0
  30. package/dist/esm/components/NetworkIcons.js +6 -0
  31. package/dist/esm/components/ReduxTreeView.d.ts +12 -3
  32. package/dist/esm/components/ReduxTreeView.js +1095 -279
  33. package/dist/esm/components/SectionHeader.d.ts +1 -1
  34. package/dist/esm/components/SectionHeader.js +8 -2
  35. package/dist/esm/constants/version.d.ts +1 -1
  36. package/dist/esm/constants/version.js +1 -1
  37. package/dist/esm/customHooks/reduxLogger.d.ts +2 -0
  38. package/dist/esm/customHooks/reduxLogger.js +2 -0
  39. package/dist/esm/helpers/index.d.ts +1 -0
  40. package/dist/esm/helpers/index.js +14 -1
  41. package/dist/esm/helpers/settingsStore.d.ts +1 -0
  42. package/dist/esm/helpers/settingsStore.js +3 -0
  43. package/dist/esm/index.js +111 -92
  44. package/dist/esm/types/index.d.ts +3 -0
  45. package/package.json +1 -1
@@ -1,361 +1,1177 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Animated, LayoutAnimation, Platform, Pressable, StyleSheet, Text, UIManager, View, } from 'react-native';
1
+ import React, { useEffect, useState, useMemo } from 'react';
2
+ import { LayoutAnimation, Platform, Pressable, StyleSheet, Text, TextInput, UIManager, View, ScrollView, ToastAndroid, Alert, } from 'react-native';
3
3
  import { AppColors } from '../styles/AppColors';
4
4
  import { AppFonts } from '../styles/AppFonts';
5
- import { ChevronIcon } from './NetworkIcons';
6
- import Svg, { Path } from 'react-native-svg';
7
- const FolderIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
8
- <Path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
9
- </Svg>);
10
- export const ReduxStoreIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
11
- <Path d="M3 9l1.5-5h15L21 9M3 9v10a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V9M3 9h18M7 9v3a2 2 0 0 0 4 0V9m2 0v3a2 2 0 0 0 4 0V9" stroke={color} strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
12
- </Svg>);
13
- const animateTreeLayout = () => {
14
- if (Platform.OS === 'ios') {
15
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
5
+ import { ChevronIcon, FolderIcon, TrashIcon, HeaderPauseIcon, CopyIcon, SearchIcon, ClearIcon, ClockIcon, ExpandCollapseIcon, SignalIcon, LayersIcon, TerminalIcon, RequestIcon, DiffIcon, SortIcon, CalendarIcon, } from './NetworkIcons';
6
+ import JsonViewer from './JsonViewer';
7
+ import DiffViewer from './DiffViewer';
8
+ import TouchableScale from './TouchableScale';
9
+ import { copyToClipboard, getSize } from '../helpers';
10
+ import { getCustomStorage } from '../helpers/settingsStore';
11
+ // Enable LayoutAnimation for Android
12
+ if (Platform.OS === 'android') {
13
+ UIManager.setLayoutAnimationEnabledExperimental?.(true);
14
+ }
15
+ const animateLayout = () => {
16
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
17
+ };
18
+ // Chevron pointing left (for back buttons)
19
+ const BackChevronIcon = ({ color = AppColors.purple, size = 12 }) => (<View style={{ transform: [{ rotate: '180deg' }], marginRight: 4 }}>
20
+ <ChevronIcon color={color} size={size}/>
21
+ </View>);
22
+ // Chevron pointing right (for list items)
23
+ const ForwardChevronIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<View style={{ transform: [{ rotate: '0deg' }], marginLeft: 'auto' }}>
24
+ <ChevronIcon color={color} size={size}/>
25
+ </View>);
26
+ // Helper: check if a value recursively contains a string term
27
+ const valueContainsTerm = (val, term) => {
28
+ if (!term)
29
+ return true;
30
+ const t = term.toLowerCase();
31
+ if (val === null || val === undefined)
32
+ return false;
33
+ if (typeof val !== 'object') {
34
+ return String(val).toLowerCase().includes(t);
35
+ }
36
+ if (Array.isArray(val)) {
37
+ return val.some(item => valueContainsTerm(item, term));
16
38
  }
39
+ return Object.keys(val).some(key => key.toLowerCase().includes(t) || valueContainsTerm(val[key], term));
17
40
  };
18
- const AnimatedChevron = ({ color, expanded, size, style, }) => {
19
- const progress = useRef(new Animated.Value(expanded ? 1 : 0)).current;
41
+ export const ReduxTreeView = ({ state, actionHistory = [], lastActionMap = {}, search = '', onSearchChange, autoRefresh = true, onToggleAutoRefresh, onClearHistory, onDetailOpenChange, selectedSliceKey = null, onSelectSliceKey, selectedActionId = null, onSelectActionId, }) => {
42
+ const [activeTab, setActiveTab] = useState('state');
43
+ const [actionSubTab, setActionSubTab] = useState('payload');
44
+ // local fallbacks if props are not provided
45
+ const [localSliceKey, setLocalSliceKey] = useState(null);
46
+ const [localActionId, setLocalActionId] = useState(null);
47
+ const activeSliceKey = onSelectSliceKey ? selectedSliceKey : localSliceKey;
48
+ const activeActionId = onSelectActionId ? selectedActionId : localActionId;
49
+ const setActiveSliceKey = (key) => {
50
+ if (onSelectSliceKey)
51
+ onSelectSliceKey(key);
52
+ else
53
+ setLocalSliceKey(key);
54
+ };
55
+ const setActiveActionId = (id) => {
56
+ if (onSelectActionId)
57
+ onSelectActionId(id);
58
+ else
59
+ setLocalActionId(id);
60
+ };
61
+ // Expand / collapse all states for detail views
62
+ const [sliceForceOpen, setSliceForceOpen] = useState(undefined);
63
+ const [actionForceOpen, setActionForceOpen] = useState(undefined);
64
+ const [searchModeType, setSearchModeType] = useState(true);
65
+ const [searchModeData, setSearchModeData] = useState(true);
66
+ // Sorting directions
67
+ const [stateSortAsc, setStateSortAsc] = useState(true);
68
+ const [actionSortAsc, setActionSortAsc] = useState(false);
69
+ // Persistence-related states for State Slice details
70
+ const [sliceDetailTab, setSliceDetailTab] = useState('live');
71
+ const [isPersisted, setIsPersisted] = useState(false);
72
+ const [persistedData, setPersistedData] = useState(null);
20
73
  useEffect(() => {
21
- Animated.timing(progress, {
22
- toValue: expanded ? 1 : 0,
23
- duration: 180,
24
- useNativeDriver: true,
25
- }).start();
26
- }, [expanded, progress]);
27
- const rotate = progress.interpolate({
28
- inputRange: [0, 1],
29
- outputRange: ['0deg', '90deg'],
30
- });
31
- return (<Animated.View style={[style, { transform: [{ rotate }] }]}>
32
- <ChevronIcon color={color} size={size}/>
33
- </Animated.View>);
34
- };
35
- const ReduxValueNode = ({ name, value, level, search, lastAction }) => {
36
- const [expanded, setExpanded] = useState(level < 1);
37
- const isObject = typeof value === 'object' && value !== null;
38
- const isArray = Array.isArray(value);
39
- const nameStr = String(name);
40
- // Filter check if search query matches key or value
41
- const matchesSearch = (k, val) => {
42
- if (!search)
43
- return true;
44
- const s = search.toLowerCase();
45
- if (k.toLowerCase().includes(s))
46
- return true;
47
- if (typeof val !== 'object' && String(val).toLowerCase().includes(s))
48
- return true;
49
- if (typeof val === 'object' && val !== null) {
50
- return Object.keys(val).some(key => matchesSearch(key, val[key]));
74
+ if (activeSliceKey === null) {
75
+ setIsPersisted(false);
76
+ setPersistedData(null);
77
+ setSliceDetailTab('live');
78
+ return;
51
79
  }
52
- return false;
53
- };
54
- if (!matchesSearch(nameStr, value)) {
55
- return null;
56
- }
57
- if (!isObject) {
58
- const valStr = value === null
59
- ? 'null'
60
- : value === undefined
61
- ? 'undefined'
62
- : String(value);
63
- let valColor = '#0D9488'; // String
64
- if (value === null || value === undefined) {
65
- valColor = AppColors.grayTextWeak;
80
+ async function checkPersistence() {
81
+ try {
82
+ const storage = getCustomStorage();
83
+ if (!storage) {
84
+ setIsPersisted(false);
85
+ setPersistedData(null);
86
+ return;
87
+ }
88
+ // Check direct key: persist:sliceKey
89
+ let raw = await storage.getItem(`persist:${activeSliceKey}`);
90
+ if (raw) {
91
+ try {
92
+ setPersistedData(JSON.parse(raw));
93
+ }
94
+ catch {
95
+ setPersistedData(raw);
96
+ }
97
+ setIsPersisted(true);
98
+ return;
99
+ }
100
+ // Check root key: persist:root
101
+ let rootRaw = await storage.getItem('persist:root');
102
+ if (rootRaw) {
103
+ const parsedRoot = JSON.parse(rootRaw);
104
+ if (parsedRoot && parsedRoot[activeSliceKey] !== undefined) {
105
+ const nested = parsedRoot[activeSliceKey];
106
+ try {
107
+ setPersistedData(typeof nested === 'string' ? JSON.parse(nested) : nested);
108
+ }
109
+ catch {
110
+ setPersistedData(nested);
111
+ }
112
+ setIsPersisted(true);
113
+ return;
114
+ }
115
+ }
116
+ }
117
+ catch (e) {
118
+ console.log('Error checking Redux persistence:', e);
119
+ }
120
+ setIsPersisted(false);
121
+ setPersistedData(null);
66
122
  }
67
- else if (typeof value === 'number') {
68
- valColor = '#D97706';
123
+ checkPersistence();
124
+ }, [activeSliceKey]);
125
+ const handleClearPersistence = () => {
126
+ Alert.alert('Clear Persisted State', `Are you sure you want to delete the persisted storage item for the slice "${activeSliceKey}"? This will clear the value from device storage, but the current active in-memory Redux state will remain unchanged until the next app reload.`, [
127
+ { text: 'Cancel', style: 'cancel' },
128
+ {
129
+ text: 'Delete',
130
+ style: 'destructive',
131
+ onPress: async () => {
132
+ try {
133
+ const storage = getCustomStorage();
134
+ if (!storage)
135
+ return;
136
+ // Delete direct key
137
+ if (typeof storage.removeItem === 'function') {
138
+ await storage.removeItem(`persist:${activeSliceKey}`);
139
+ }
140
+ else {
141
+ await storage.setItem(`persist:${activeSliceKey}`, '');
142
+ }
143
+ // Delete from root key
144
+ let rootRaw = await storage.getItem('persist:root');
145
+ if (rootRaw) {
146
+ const parsed = JSON.parse(rootRaw);
147
+ if (parsed && parsed[activeSliceKey] !== undefined) {
148
+ delete parsed[activeSliceKey];
149
+ await storage.setItem('persist:root', JSON.stringify(parsed));
150
+ }
151
+ }
152
+ setIsPersisted(false);
153
+ setPersistedData(null);
154
+ setSliceDetailTab('live');
155
+ if (Platform.OS === 'android') {
156
+ ToastAndroid.show('Persisted state cleared', ToastAndroid.SHORT);
157
+ }
158
+ else {
159
+ Alert.alert('Cleared', 'Persisted state has been cleared from device storage.');
160
+ }
161
+ }
162
+ catch (err) {
163
+ Alert.alert('Error', 'Failed to clear persisted state.');
164
+ }
165
+ }
166
+ }
167
+ ]);
168
+ };
169
+ const isSearching = search.trim().length > 0;
170
+ useEffect(() => {
171
+ onDetailOpenChange?.(activeSliceKey !== null || activeActionId !== null);
172
+ return () => {
173
+ onDetailOpenChange?.(false);
174
+ };
175
+ }, [activeSliceKey, activeActionId, onDetailOpenChange]);
176
+ // Reset details view when tab changes
177
+ const switchTab = (tab) => {
178
+ animateLayout();
179
+ setActiveTab(tab);
180
+ setActiveSliceKey(null);
181
+ setActiveActionId(null);
182
+ setSliceForceOpen(undefined);
183
+ setActionForceOpen(undefined);
184
+ };
185
+ const handleGoBackSlice = () => {
186
+ animateLayout();
187
+ setActiveSliceKey(null);
188
+ setSliceForceOpen(undefined);
189
+ };
190
+ const handleGoBackAction = () => {
191
+ animateLayout();
192
+ setActiveActionId(null);
193
+ setActionForceOpen(undefined);
194
+ };
195
+ // Filtered state keys based on search query
196
+ const filteredStateKeys = useMemo(() => {
197
+ if (!state || typeof state !== 'object')
198
+ return [];
199
+ const keys = Object.keys(state);
200
+ let result = keys;
201
+ if (isSearching) {
202
+ result = keys.filter(key => {
203
+ const typeMatch = searchModeType && key.toLowerCase().includes(search.toLowerCase());
204
+ const dataMatch = searchModeData && valueContainsTerm(state[key], search);
205
+ return typeMatch || dataMatch;
206
+ });
69
207
  }
70
- else if (typeof value === 'boolean') {
71
- valColor = '#4F46E5';
208
+ return [...result].sort((a, b) => {
209
+ const comparison = a.localeCompare(b);
210
+ return stateSortAsc ? comparison : -comparison;
211
+ });
212
+ }, [state, search, isSearching, searchModeType, searchModeData, stateSortAsc]);
213
+ // Filtered action history based on search query
214
+ const filteredActionHistory = useMemo(() => {
215
+ if (!actionHistory)
216
+ return [];
217
+ let result = actionHistory;
218
+ if (isSearching) {
219
+ result = actionHistory.filter(action => {
220
+ const typeMatch = searchModeType && action.type.toLowerCase().includes(search.toLowerCase());
221
+ const payloadMatch = searchModeData && action.payload && valueContainsTerm(action.payload, search);
222
+ return typeMatch || payloadMatch;
223
+ });
72
224
  }
73
- return (<View style={[reduxValueStyles.treeRow, { paddingLeft: 12 }]}>
74
- <View style={reduxValueStyles.treeLeafConnector}/>
75
- <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, flexWrap: 'wrap', gap: 6 }}>
76
- <Text style={reduxValueStyles.keyText} selectable={true}>
77
- {nameStr}
78
- <Text style={reduxValueStyles.colonText}>: </Text>
79
- <Text style={[reduxValueStyles.valText, { color: valColor }]} selectable={true}>
80
- {valStr}
81
- </Text>
82
- </Text>
83
- {lastAction && (<View style={styles.actionTypeBadge}>
84
- <Text style={styles.actionTypeText} numberOfLines={1}>
85
- {lastAction.type}
225
+ return [...result].sort((a, b) => {
226
+ const comparison = a.id - b.id;
227
+ return actionSortAsc ? comparison : -comparison;
228
+ });
229
+ }, [actionHistory, search, isSearching, searchModeType, searchModeData, actionSortAsc]);
230
+ // Find currently selected action object
231
+ const selectedAction = useMemo(() => {
232
+ if (activeActionId === null)
233
+ return null;
234
+ return actionHistory.find(a => a.id === activeActionId) || null;
235
+ }, [activeActionId, actionHistory]);
236
+ if (!state || typeof state !== 'object') {
237
+ return (<View style={styles.errorContainer}>
238
+ <Text style={styles.errorText}>No connected Redux store state found.</Text>
239
+ </View>);
240
+ }
241
+ // ── Render Detail View: State Slice ──
242
+ if (activeTab === 'state' && activeSliceKey !== null) {
243
+ const sliceData = state[activeSliceKey];
244
+ const fieldsCount = typeof sliceData === 'object' && sliceData !== null
245
+ ? Object.keys(sliceData).length
246
+ : 1;
247
+ return (<View style={{ flex: 1 }}>
248
+ {/* Non-scrollable header */}
249
+ <View style={{ paddingHorizontal: 6, paddingTop: 4 }}>
250
+ <View style={styles.apiLikeInfoBar}>
251
+ <View style={styles.apiBadgeRow}>
252
+ <View style={[styles.apiMethodBadge, { backgroundColor: AppColors.purple }]}>
253
+ <Text style={styles.apiMethodBadgeText}>REDUX</Text>
254
+ </View>
255
+ <View style={[styles.apiMethodBadge, { backgroundColor: AppColors.purpleShade50, borderWidth: 1, borderColor: AppColors.purple + '30' }]}>
256
+ <Text style={[styles.apiMethodBadgeText, { color: AppColors.purple }]}>SLICE</Text>
257
+ </View>
258
+ {isPersisted && (<View style={[styles.apiMethodBadge, { backgroundColor: '#10B981' }]}>
259
+ <Text style={styles.apiMethodBadgeText}>PERSISTED</Text>
260
+ </View>)}
261
+ <View style={styles.apiChip}>
262
+ <Text style={styles.apiChipText}>{getSize(sliceData)}</Text>
263
+ </View>
264
+ <View style={styles.apiChip}>
265
+ <Text style={styles.apiChipText}>{fieldsCount} {fieldsCount === 1 ? 'field' : 'fields'}</Text>
266
+ </View>
267
+ </View>
268
+
269
+ <Text style={styles.apiTitleText} selectable={true}>{activeSliceKey}</Text>
270
+
271
+ <View style={styles.apiMetaRow}>
272
+ <ClockIcon color={AppColors.grayTextWeak} size={10}/>
273
+ <Text style={styles.apiMetaText}>
274
+ Last updated: {lastActionMap[activeSliceKey] ? lastActionMap[activeSliceKey].timestamp : 'Initial'}
86
275
  </Text>
87
- </View>)}
276
+ </View>
277
+ </View>
278
+ </View>
279
+
280
+ {/* Segment Control */}
281
+ <View style={{
282
+ flexDirection: 'row',
283
+ backgroundColor: AppColors.grayBackground,
284
+ borderRadius: 10,
285
+ padding: 3,
286
+ marginHorizontal: 6,
287
+ marginBottom: 10,
288
+ marginTop: 6,
289
+ borderWidth: 1,
290
+ borderColor: AppColors.grayBorderSecondary,
291
+ }}>
292
+ {(isPersisted ? ['live', 'persisted'] : ['live']).map(tab => {
293
+ const isActive = sliceDetailTab === tab;
294
+ const getLabel = () => {
295
+ if (tab === 'live')
296
+ return 'Live State';
297
+ return `Persisted (${getSize(persistedData)})`;
298
+ };
299
+ const getIcon = () => {
300
+ const iconColor = isActive ? '#FFFFFF' : AppColors.grayText;
301
+ if (tab === 'live')
302
+ return <LayersIcon color={iconColor} size={11}/>;
303
+ return <CalendarIcon color={iconColor} size={11}/>;
304
+ };
305
+ return (<Pressable key={tab} onPress={() => {
306
+ animateLayout();
307
+ setSliceDetailTab(tab);
308
+ }} style={{
309
+ flex: 1,
310
+ paddingVertical: 6,
311
+ flexDirection: 'row',
312
+ alignItems: 'center',
313
+ justifyContent: 'center',
314
+ borderRadius: 8,
315
+ backgroundColor: isActive ? AppColors.purple : 'transparent',
316
+ gap: 4,
317
+ }}>
318
+ {getIcon()}
319
+ <Text style={{
320
+ fontFamily: AppFonts.interBold,
321
+ fontSize: 10,
322
+ color: isActive ? '#FFFFFF' : AppColors.grayText,
323
+ }}>
324
+ {getLabel()}
325
+ </Text>
326
+ </Pressable>);
327
+ })}
328
+ </View>
329
+
330
+ {/* Scrollable Tab Content */}
331
+ <View style={{ flex: 1, paddingHorizontal: 6 }}>
332
+ {/* Back button + controls */}
333
+ <View style={styles.contentDetailSubHeader}>
334
+ <TouchableScale onPress={handleGoBackSlice} style={styles.backBtn} hitSlop={12}>
335
+ <BackChevronIcon color={AppColors.purple} size={11}/>
336
+ <Text style={styles.backBtnText}>State Slices</Text>
337
+ </TouchableScale>
338
+
339
+ <View style={styles.detailHeaderActions}>
340
+ {isPersisted && sliceDetailTab === 'persisted' && (<TouchableScale onPress={handleClearPersistence} style={[styles.detailHeaderBtn, { marginRight: 8 }]} hitSlop={8}>
341
+ <TrashIcon color={AppColors.errorColor} size={14}/>
342
+ </TouchableScale>)}
343
+
344
+ <TouchableScale onPress={() => {
345
+ animateLayout();
346
+ setSliceForceOpen(prev => (prev === true ? false : prev === false ? undefined : true));
347
+ }} style={styles.detailHeaderBtn} hitSlop={8}>
348
+ <ExpandCollapseIcon expanded={sliceForceOpen !== false} color={AppColors.purple} size={14}/>
349
+ </TouchableScale>
350
+
351
+ <TouchableScale onPress={() => {
352
+ const dataToCopy = sliceDetailTab === 'live' ? sliceData : persistedData;
353
+ copyToClipboard(dataToCopy, `Redux Slice '${activeSliceKey}' (${sliceDetailTab})`);
354
+ }} style={styles.detailHeaderBtn} hitSlop={8}>
355
+ <CopyIcon color={AppColors.purple} size={14}/>
356
+ </TouchableScale>
357
+ </View>
358
+ </View>
359
+
360
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 24 }} showsVerticalScrollIndicator={true}>
361
+ <View style={{ padding: 12 }}>
362
+ <JsonViewer data={sliceDetailTab === 'live' ? sliceData : (persistedData ?? 'No persisted data')} search={search} forceOpen={sliceForceOpen !== undefined ? sliceForceOpen : (isSearching ? true : undefined)} fullHeight={true}/>
363
+ </View>
364
+ </ScrollView>
88
365
  </View>
89
366
  </View>);
90
367
  }
91
- const keys = Object.keys(value);
92
- const summaryText = isArray
93
- ? `Array [${keys.length}]`
94
- : `Object {${keys.length}}`;
95
- return (<View style={reduxValueStyles.treeNodeBlock}>
96
- <Pressable onPress={() => {
97
- animateTreeLayout();
98
- setExpanded(!expanded);
99
- }} style={reduxValueStyles.treeRow}>
100
- <View style={reduxValueStyles.treeLeafConnector}/>
101
- <AnimatedChevron color={AppColors.grayTextWeak} expanded={expanded} size={10} style={reduxValueStyles.chevronWrap}/>
102
- <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, flexWrap: 'wrap', gap: 6 }}>
103
- <Text style={reduxValueStyles.keyText} selectable={true}>
104
- {nameStr}
105
- <Text style={reduxValueStyles.colonText}>: </Text>
106
- <Text style={reduxValueStyles.summaryText}>{summaryText}</Text>
107
- </Text>
108
- {lastAction && (<View style={styles.actionTypeBadge}>
109
- <Text style={styles.actionTypeText} numberOfLines={1}>
110
- {lastAction.type}
111
- </Text>
112
- </View>)}
368
+ // ── Render Detail View: Timeline Action ──
369
+ if (activeTab === 'timeline' && selectedAction !== null) {
370
+ const affectedSlices = selectedAction.affectedSlices || [];
371
+ return (<View style={{ flex: 1 }}>
372
+ {/* Non-scrollable header */}
373
+ <View style={{ paddingHorizontal: 6, paddingTop: 4 }}>
374
+ <View style={styles.apiLikeInfoBar}>
375
+ <View style={styles.apiBadgeRow}>
376
+ <View style={[styles.apiMethodBadge, { backgroundColor: AppColors.purple }]}>
377
+ <Text style={styles.apiMethodBadgeText}>REDUX</Text>
378
+ </View>
379
+ <View style={[styles.apiMethodBadge, { backgroundColor: '#F59E0B' }]}>
380
+ <Text style={styles.apiMethodBadgeText}>ACTION</Text>
381
+ </View>
382
+ <View style={styles.apiChip}>
383
+ <Text style={styles.apiChipText}>#{selectedAction.id}</Text>
384
+ </View>
385
+ <View style={styles.apiChip}>
386
+ <Text style={styles.apiChipText}>{getSize(selectedAction.payload)}</Text>
387
+ </View>
388
+ </View>
389
+
390
+ <Text style={styles.apiTitleText} selectable={true}>{selectedAction.type}</Text>
391
+
392
+ <View style={styles.apiMetaRow}>
393
+ <ClockIcon color={AppColors.grayTextWeak} size={10}/>
394
+ <Text style={styles.apiMetaText}>Dispatched: {selectedAction.timestamp}</Text>
395
+ </View>
396
+ </View>
113
397
  </View>
114
- </Pressable>
115
- {expanded && (<View style={reduxValueStyles.treeChildrenContainer}>
116
- {keys.map(key => (<ReduxValueNode key={key} name={key} value={value[key]} level={level + 1} search={search}/>))}
117
- </View>)}
118
- </View>);
119
- };
120
- export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
121
- const [storeExpanded, setStoreExpanded] = useState(true);
122
- const [reducerExpanded, setReducerExpanded] = useState({});
123
- useEffect(() => {
124
- if (Platform.OS === 'android') {
125
- UIManager.setLayoutAnimationEnabledExperimental?.(true);
126
- }
127
- }, []);
128
- if (!state || typeof state !== 'object') {
129
- return (<Text style={{
130
- fontFamily: AppFonts.interRegular,
131
- fontSize: 12,
132
- color: AppColors.grayTextWeak,
133
- padding: 12,
398
+
399
+ {/* Affected Slices Tags list */}
400
+ {affectedSlices.length > 0 && (<View style={styles.detailAffectedTags}>
401
+ <Text style={styles.affectedTagsLabel}>AFFECTED SLICES:</Text>
402
+ <View style={styles.detailTagsList}>
403
+ {affectedSlices.map(slice => (<View key={slice} style={styles.sliceTag}>
404
+ <Text style={styles.sliceTagText}>{slice}</Text>
405
+ </View>))}
406
+ </View>
407
+ </View>)}
408
+
409
+ {/* Segment Control */}
410
+ <View style={{
411
+ flexDirection: 'row',
412
+ backgroundColor: AppColors.grayBackground,
413
+ borderRadius: 10,
414
+ padding: 3,
415
+ marginHorizontal: 6,
416
+ marginBottom: 10,
417
+ marginTop: 6,
418
+ borderWidth: 1,
419
+ borderColor: AppColors.grayBorderSecondary,
134
420
  }}>
135
- No state object to display.
136
- </Text>);
421
+ {['payload', 'diff', 'state'].map(tab => {
422
+ const isActive = actionSubTab === tab;
423
+ const getLabel = () => {
424
+ if (tab === 'payload')
425
+ return 'Payload';
426
+ if (tab === 'diff')
427
+ return 'Diff Changes';
428
+ return 'Snapshot';
429
+ };
430
+ const getIcon = () => {
431
+ const iconColor = isActive ? '#FFFFFF' : AppColors.grayText;
432
+ if (tab === 'payload')
433
+ return <RequestIcon color={iconColor} size={11}/>;
434
+ if (tab === 'diff')
435
+ return <DiffIcon color={iconColor} size={11}/>;
436
+ return <LayersIcon color={iconColor} size={11}/>;
437
+ };
438
+ return (<Pressable key={tab} onPress={() => {
439
+ animateLayout();
440
+ setActionSubTab(tab);
441
+ }} style={{
442
+ flex: 1,
443
+ paddingVertical: 6,
444
+ flexDirection: 'row',
445
+ alignItems: 'center',
446
+ justifyContent: 'center',
447
+ borderRadius: 8,
448
+ backgroundColor: isActive ? AppColors.purple : 'transparent',
449
+ gap: 4,
450
+ }}>
451
+ {getIcon()}
452
+ <Text style={{
453
+ fontFamily: AppFonts.interBold,
454
+ fontSize: 10,
455
+ color: isActive ? '#FFFFFF' : AppColors.grayText,
456
+ }}>
457
+ {getLabel()}
458
+ </Text>
459
+ </Pressable>);
460
+ })}
461
+ </View>
462
+
463
+ {/* Scrollable Tab Content */}
464
+ <View style={{ flex: 1, paddingHorizontal: 6 }}>
465
+ {/* Back button + controls */}
466
+ <View style={styles.contentDetailSubHeader}>
467
+ <TouchableScale onPress={handleGoBackAction} style={styles.backBtn} hitSlop={12}>
468
+ <BackChevronIcon color={AppColors.purple} size={11}/>
469
+ <Text style={styles.backBtnText}>Actions</Text>
470
+ </TouchableScale>
471
+
472
+ {(actionSubTab === 'payload' || actionSubTab === 'state') && (<View style={styles.detailHeaderActions}>
473
+ <TouchableScale onPress={() => {
474
+ animateLayout();
475
+ setActionForceOpen(prev => (prev === true ? false : prev === false ? undefined : true));
476
+ }} style={styles.detailHeaderBtn} hitSlop={8}>
477
+ <ExpandCollapseIcon expanded={actionForceOpen !== false} color={AppColors.purple} size={14}/>
478
+ </TouchableScale>
479
+
480
+ <TouchableScale onPress={() => {
481
+ const dataToCopy = actionSubTab === 'payload' ? selectedAction.payload : selectedAction.nextState;
482
+ copyToClipboard(dataToCopy, actionSubTab === 'payload' ? 'Action payload' : 'State snapshot');
483
+ }} style={styles.detailHeaderBtn} hitSlop={8}>
484
+ <CopyIcon color={AppColors.purple} size={14}/>
485
+ </TouchableScale>
486
+ </View>)}
487
+ </View>
488
+
489
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 24 }} showsVerticalScrollIndicator={true}>
490
+ {actionSubTab === 'payload' && (<View style={{ padding: 12 }}>
491
+ <JsonViewer data={selectedAction.payload ?? 'No payload data'} wrap forceOpen={actionForceOpen !== undefined ? actionForceOpen : undefined} fullHeight={true}/>
492
+ </View>)}
493
+
494
+ {actionSubTab === 'diff' && (<View style={{ padding: 12 }}>
495
+ <DiffViewer oldData={selectedAction.prevState} newData={selectedAction.nextState}/>
496
+ </View>)}
497
+
498
+ {actionSubTab === 'state' && (<View style={{ padding: 12 }}>
499
+ <JsonViewer data={selectedAction.nextState} wrap forceOpen={actionForceOpen !== undefined ? actionForceOpen : undefined} fullHeight={true}/>
500
+ </View>)}
501
+ </ScrollView>
502
+ </View>
503
+ </View>);
137
504
  }
138
- const reducers = Object.keys(state);
139
- const toggleReducer = (key) => {
140
- animateTreeLayout();
141
- setReducerExpanded(prev => ({
142
- ...prev,
143
- [key]: !prev[key],
144
- }));
145
- };
505
+ // ── Render List Views (Default) ──
146
506
  return (<View style={styles.container}>
147
- {/* Root Node: Store */}
148
- <Pressable onPress={() => {
149
- animateTreeLayout();
150
- setStoreExpanded(!storeExpanded);
151
- }} style={styles.storeHeader}>
152
- <AnimatedChevron color="#FFFFFF" expanded={storeExpanded} size={12} style={styles.chevronWrap}/>
153
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
154
- <ReduxStoreIcon color="#FFFFFF" size={14}/>
155
- <Text style={styles.storeTitle}>Redux Store</Text>
507
+ {/* ── Control Bar ── */}
508
+ <View style={styles.toolbar}>
509
+ <View style={styles.statusSection}>
510
+ <View style={[styles.statusBadge, autoRefresh ? styles.statusBadgeActive : styles.statusBadgePaused]}>
511
+ <View style={[styles.statusDot, autoRefresh ? styles.statusDotActive : styles.statusDotPaused]}/>
512
+ <Text style={[styles.statusBadgeText, autoRefresh ? styles.statusTextActive : styles.statusTextPaused]}>
513
+ {autoRefresh ? 'LIVE' : 'PAUSED'}
514
+ </Text>
515
+ </View>
156
516
  </View>
157
- <View style={styles.badge}>
158
- <Text style={styles.badgeText}>{reducers.length} Slices</Text>
517
+
518
+ <View style={styles.toolbarActions}>
519
+ {/* Sorting Toggle Button */}
520
+ <TouchableScale onPress={() => {
521
+ animateLayout();
522
+ if (activeTab === 'state') {
523
+ setStateSortAsc(prev => !prev);
524
+ }
525
+ else {
526
+ setActionSortAsc(prev => !prev);
527
+ }
528
+ }} style={styles.iconBtn} hitSlop={8}>
529
+ <SortIcon ascending={activeTab === 'state' ? stateSortAsc : actionSortAsc} color={AppColors.purple} size={15}/>
530
+ </TouchableScale>
531
+
532
+ {onToggleAutoRefresh && (<TouchableScale onPress={onToggleAutoRefresh} style={styles.iconBtn} hitSlop={8}>
533
+ <HeaderPauseIcon isPaused={!autoRefresh} color={AppColors.purple} size={15}/>
534
+ </TouchableScale>)}
535
+ {onClearHistory && (<TouchableScale onPress={onClearHistory} style={[styles.iconBtn, { borderColor: AppColors.errorColor + '40', backgroundColor: AppColors.errorColor + '08' }]} hitSlop={8}>
536
+ <TrashIcon color={AppColors.errorColor} size={15}/>
537
+ </TouchableScale>)}
159
538
  </View>
160
- </Pressable>
539
+ </View>
540
+
541
+ {/* ── Search Bar ── */}
542
+ {onSearchChange && (<View style={styles.searchContainer}>
543
+ <View style={styles.searchBar}>
544
+ <SearchIcon color={AppColors.grayTextWeak} size={14}/>
545
+ <TextInput placeholder="Search keys, values, or action payloads..." placeholderTextColor={AppColors.grayTextWeak} value={search} onChangeText={onSearchChange} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
546
+ {search.length > 0 && (<TouchableScale onPress={() => onSearchChange('')} hitSlop={10}>
547
+ <ClearIcon color={AppColors.grayTextWeak} size={14}/>
548
+ </TouchableScale>)}
549
+ </View>
550
+ <View style={styles.filterChipRow}>
551
+ <Text style={styles.filterChipLabel}>FILTER BY:</Text>
552
+ <Pressable onPress={() => {
553
+ animateLayout();
554
+ if (searchModeType && !searchModeData)
555
+ return; // Prevent disabling both
556
+ setSearchModeType(!searchModeType);
557
+ }} style={[styles.filterChip, searchModeType && styles.filterChipActive]}>
558
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
559
+ <SignalIcon color={searchModeType ? AppColors.purple : AppColors.grayText} size={11}/>
560
+ <Text style={[styles.filterChipText, searchModeType && styles.filterChipTextActive]}>
561
+ Type / Key
562
+ </Text>
563
+ </View>
564
+ </Pressable>
565
+ <Pressable onPress={() => {
566
+ animateLayout();
567
+ if (!searchModeType && searchModeData)
568
+ return; // Prevent disabling both
569
+ setSearchModeData(!searchModeData);
570
+ }} style={[styles.filterChip, searchModeData && styles.filterChipActive]}>
571
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
572
+ <LayersIcon color={searchModeData ? AppColors.purple : AppColors.grayText} size={11}/>
573
+ <Text style={[styles.filterChipText, searchModeData && styles.filterChipTextActive]}>
574
+ Payload Data
575
+ </Text>
576
+ </View>
577
+ </Pressable>
578
+ </View>
579
+ </View>)}
161
580
 
162
- {storeExpanded && (<View style={styles.storeChildren}>
163
- {reducers.map((reducerKey, index) => {
164
- const isLastReducer = index === reducers.length - 1;
165
- const isExpanded = !!reducerExpanded[reducerKey];
581
+ {/* ── Tab Selector ── */}
582
+ <View style={styles.tabContainer}>
583
+ <Pressable style={[styles.tabButton, activeTab === 'state' && styles.tabButtonActive]} onPress={() => switchTab('state')}>
584
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
585
+ <LayersIcon color={activeTab === 'state' ? AppColors.purple : AppColors.grayText} size={12}/>
586
+ <Text style={[styles.tabButtonText, activeTab === 'state' && styles.tabButtonTextActive]}>
587
+ State Slices ({filteredStateKeys.length})
588
+ </Text>
589
+ </View>
590
+ </Pressable>
591
+ <Pressable style={[styles.tabButton, activeTab === 'timeline' && styles.tabButtonActive]} onPress={() => switchTab('timeline')}>
592
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
593
+ <TerminalIcon color={activeTab === 'timeline' ? AppColors.purple : AppColors.grayText} size={12}/>
594
+ <Text style={[styles.tabButtonText, activeTab === 'timeline' && styles.tabButtonTextActive]}>
595
+ Actions ({filteredActionHistory.length})
596
+ </Text>
597
+ </View>
598
+ </Pressable>
599
+ </View>
600
+
601
+ {/* ── State List View ── */}
602
+ {activeTab === 'state' && (<ScrollView style={styles.scrollList} contentContainerStyle={styles.scrollContent}>
603
+ {filteredStateKeys.length === 0 ? (<View style={styles.emptyState}>
604
+ <Text style={styles.emptyStateText}>
605
+ {isSearching ? 'No store slices match your search.' : 'Redux store contains no state.'}
606
+ </Text>
607
+ </View>) : (filteredStateKeys.map(reducerKey => {
166
608
  const sliceData = state[reducerKey];
609
+ const fieldsCount = typeof sliceData === 'object' && sliceData !== null
610
+ ? Object.keys(sliceData).length
611
+ : 1;
167
612
  const lastAction = lastActionMap[reducerKey];
168
- return (<View key={reducerKey} style={styles.reducerContainer}>
169
- {/* Visual Branch Line for Reducer */}
170
- <View style={[
171
- styles.reducerVerticalLine,
172
- isLastReducer && !isExpanded && { bottom: '50%' },
173
- ]}/>
174
-
175
- {/* Reducer Header */}
176
- <Pressable onPress={() => toggleReducer(reducerKey)} style={styles.reducerHeader}>
177
- <View style={styles.reducerHorizontalLine}/>
178
- <AnimatedChevron color={AppColors.purple} expanded={isExpanded} size={10} style={styles.chevronWrap}/>
179
- <View style={styles.iconWrap}>
613
+ const lastUpdatedTime = lastAction ? lastAction.timestamp : 'Initial';
614
+ return (<TouchableScale key={reducerKey} onPress={() => {
615
+ animateLayout();
616
+ setActiveSliceKey(reducerKey);
617
+ }} style={styles.listRow}>
618
+ <View style={styles.sliceIcon}>
180
619
  <FolderIcon color={AppColors.purple} size={11}/>
181
620
  </View>
182
- <View style={{ flexDirection: 'row', alignItems: 'center', flex: 1, flexWrap: 'wrap', gap: 6 }}>
183
- <Text style={styles.reducerText}>{reducerKey}</Text>
184
- {lastAction && (<View style={styles.actionTypeBadge}>
185
- <Text style={styles.actionTypeText} numberOfLines={1}>
186
- {lastAction.type}
187
- </Text>
188
- </View>)}
621
+ <View style={styles.rowMain}>
622
+ <Text style={styles.rowTitle}>{reducerKey}</Text>
623
+ <Text style={styles.rowSub}>
624
+ {getSize(sliceData)} {fieldsCount} {fieldsCount === 1 ? 'field' : 'fields'} • Updated: {lastUpdatedTime}
625
+ </Text>
189
626
  </View>
190
- </Pressable>
627
+ <ForwardChevronIcon color={AppColors.grayTextWeak} size={10}/>
628
+ </TouchableScale>);
629
+ }))}
630
+ </ScrollView>)}
191
631
 
192
- {isExpanded && (<View style={styles.reducerChildren}>
193
- {/* Vertical line connecting children */}
194
- <View style={styles.childrenVerticalLine}/>
195
- {typeof sliceData === 'object' && sliceData !== null ? (Object.keys(sliceData).map(k => (<ReduxValueNode key={k} name={k} value={sliceData[k]} level={0} search={search}/>))) : (<ReduxValueNode name={reducerKey} value={sliceData} level={0} search={search} lastAction={lastAction}/>)}
196
- </View>)}
197
- </View>);
198
- })}
199
- </View>)}
632
+ {/* ── Timeline List View ── */}
633
+ {activeTab === 'timeline' && (<ScrollView style={styles.scrollList} contentContainerStyle={styles.scrollContent}>
634
+ {filteredActionHistory.length === 0 ? (<View style={styles.emptyState}>
635
+ <Text style={styles.emptyStateText}>
636
+ {isSearching
637
+ ? 'No actions match your search.'
638
+ : 'No actions dispatched yet. Trigger an action in the app to populate history.'}
639
+ </Text>
640
+ </View>) : (filteredActionHistory.map((action, index) => {
641
+ const id = action.id;
642
+ const affectedSlices = action.affectedSlices || [];
643
+ return (<TouchableScale key={id} onPress={() => {
644
+ animateLayout();
645
+ setActiveActionId(id);
646
+ }} style={styles.listRow}>
647
+ <View style={styles.seqBadge}>
648
+ <Text style={styles.seqText}>#{action.id}</Text>
649
+ </View>
650
+ <View style={styles.rowMain}>
651
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
652
+ <TerminalIcon color={AppColors.purple} size={11}/>
653
+ <Text style={styles.actionTitleText} numberOfLines={1}>{action.type}</Text>
654
+ </View>
655
+ <View style={styles.actionMetaRow}>
656
+ <ClockIcon color={AppColors.grayTextWeak} size={9}/>
657
+ <Text style={styles.actionTimeText}>{action.timestamp} • {getSize(action.payload)}</Text>
658
+ </View>
659
+ {affectedSlices.length > 0 && (<View style={styles.tagsContainer}>
660
+ {affectedSlices.map(slice => (<View key={slice} style={styles.sliceTagMini}>
661
+ <Text style={styles.sliceTagMiniText}>{slice}</Text>
662
+ </View>))}
663
+ </View>)}
664
+ </View>
665
+ <ForwardChevronIcon color={AppColors.grayTextWeak} size={10}/>
666
+ </TouchableScale>);
667
+ }))}
668
+ </ScrollView>)}
200
669
  </View>);
201
670
  };
202
- const reduxValueStyles = StyleSheet.create({
203
- treeNodeBlock: {
204
- marginTop: 4,
671
+ const styles = StyleSheet.create({
672
+ container: {
673
+ flex: 1,
674
+ backgroundColor: AppColors.grayBackground,
675
+ },
676
+ scrollList: {
677
+ flex: 1,
678
+ },
679
+ errorContainer: {
680
+ padding: 24,
681
+ alignItems: 'center',
682
+ },
683
+ errorText: {
684
+ fontFamily: AppFonts.interRegular,
685
+ fontSize: 12,
686
+ color: AppColors.grayTextWeak,
687
+ },
688
+ toolbar: {
689
+ flexDirection: 'row',
690
+ alignItems: 'center',
691
+ justifyContent: 'space-between',
692
+ paddingHorizontal: 12,
693
+ paddingVertical: 10,
694
+ backgroundColor: AppColors.primaryLight,
695
+ borderBottomWidth: 1,
696
+ borderBottomColor: AppColors.dividerColor,
205
697
  },
206
- treeRow: {
698
+ statusSection: {
207
699
  flexDirection: 'row',
208
700
  alignItems: 'center',
701
+ },
702
+ statusBadge: {
703
+ flexDirection: 'row',
704
+ alignItems: 'center',
705
+ paddingHorizontal: 8,
209
706
  paddingVertical: 3,
210
- gap: 4,
211
- position: 'relative',
212
- },
213
- treeLeafConnector: {
214
- position: 'absolute',
215
- left: -12,
216
- top: '50%',
217
- width: 8,
218
- height: 1,
707
+ borderRadius: 12,
708
+ borderWidth: 1,
709
+ },
710
+ statusBadgeActive: {
711
+ backgroundColor: AppColors.greenStatus,
712
+ borderColor: AppColors.greenColor + '30',
713
+ },
714
+ statusBadgePaused: {
715
+ backgroundColor: AppColors.grayBackground,
716
+ borderColor: AppColors.grayBorderSecondary,
717
+ },
718
+ statusDot: {
719
+ width: 6,
720
+ height: 6,
721
+ borderRadius: 3,
722
+ marginRight: 6,
723
+ },
724
+ statusDotActive: {
725
+ backgroundColor: AppColors.greenBaggageText,
726
+ },
727
+ statusDotPaused: {
728
+ backgroundColor: AppColors.grayTextWeak,
729
+ },
730
+ statusBadgeText: {
731
+ fontFamily: AppFonts.interBold,
732
+ fontSize: 9,
733
+ letterSpacing: 0.5,
734
+ },
735
+ statusTextActive: {
736
+ color: AppColors.greenBaggageText,
737
+ },
738
+ statusTextPaused: {
739
+ color: AppColors.grayText,
740
+ },
741
+ toolbarActions: {
742
+ flexDirection: 'row',
743
+ alignItems: 'center',
744
+ gap: 8,
745
+ },
746
+ iconBtn: {
747
+ width: 28,
748
+ height: 28,
749
+ borderRadius: 6,
750
+ borderWidth: 1,
751
+ borderColor: AppColors.purple + '40',
752
+ backgroundColor: AppColors.purpleShade50,
753
+ alignItems: 'center',
754
+ justifyContent: 'center',
755
+ },
756
+ searchContainer: {
757
+ backgroundColor: AppColors.primaryLight,
219
758
  borderBottomWidth: 1,
220
759
  borderBottomColor: AppColors.dividerColor,
221
- opacity: 0.5,
760
+ paddingBottom: 10,
761
+ },
762
+ searchBar: {
763
+ flexDirection: 'row',
764
+ alignItems: 'center',
765
+ backgroundColor: AppColors.grayBackground,
766
+ borderRadius: 8,
767
+ marginHorizontal: 12,
768
+ marginTop: 10,
769
+ marginBottom: 8,
770
+ paddingHorizontal: 10,
771
+ borderWidth: 1,
772
+ borderColor: AppColors.dividerColor,
773
+ height: 36,
222
774
  },
223
- treeChildrenContainer: {
224
- paddingLeft: 14,
225
- borderLeftWidth: 1,
226
- borderLeftColor: AppColors.dividerColor,
775
+ filterChipRow: {
776
+ flexDirection: 'row',
777
+ alignItems: 'center',
778
+ paddingHorizontal: 12,
779
+ gap: 8,
780
+ },
781
+ filterChipLabel: {
782
+ fontFamily: AppFonts.interBold,
783
+ fontSize: 9,
784
+ color: AppColors.grayTextWeak,
785
+ letterSpacing: 0.5,
786
+ marginRight: 2,
787
+ },
788
+ filterChip: {
789
+ paddingHorizontal: 10,
790
+ paddingVertical: 4,
791
+ borderRadius: 12,
792
+ backgroundColor: AppColors.grayBackground,
793
+ borderWidth: 1,
794
+ borderColor: AppColors.grayBorderSecondary,
795
+ },
796
+ filterChipActive: {
797
+ backgroundColor: AppColors.purpleShade50,
798
+ borderColor: AppColors.purple,
799
+ },
800
+ filterChipText: {
801
+ fontFamily: AppFonts.interMedium,
802
+ fontSize: 10,
803
+ color: AppColors.grayText,
804
+ },
805
+ filterChipTextActive: {
806
+ color: AppColors.purple,
807
+ fontFamily: AppFonts.interBold,
808
+ },
809
+ searchInput: {
810
+ flex: 1,
811
+ fontFamily: AppFonts.interRegular,
812
+ fontSize: 12,
813
+ color: AppColors.grayTextStrong,
227
814
  marginLeft: 6,
815
+ padding: 0,
816
+ },
817
+ tabContainer: {
818
+ flexDirection: 'row',
819
+ backgroundColor: AppColors.dividerColor,
820
+ borderRadius: 8,
821
+ padding: 3,
822
+ marginHorizontal: 12,
823
+ marginTop: 10,
824
+ marginBottom: 10,
228
825
  },
229
- chevronWrap: {
230
- width: 10,
231
- height: 10,
826
+ tabButton: {
827
+ flex: 1,
828
+ paddingVertical: 7,
232
829
  alignItems: 'center',
233
830
  justifyContent: 'center',
831
+ borderRadius: 6,
234
832
  },
235
- keyText: {
833
+ tabButtonActive: {
834
+ backgroundColor: AppColors.primaryLight,
835
+ shadowColor: AppColors.shadowColorString,
836
+ shadowOffset: { width: 0, height: 1 },
837
+ shadowOpacity: 0.08,
838
+ shadowRadius: 2,
839
+ elevation: 2,
840
+ },
841
+ tabButtonText: {
236
842
  fontFamily: AppFonts.interMedium,
237
- fontSize: 11.5,
238
- color: AppColors.grayTextStrong,
843
+ fontSize: 12,
844
+ color: AppColors.grayText,
239
845
  },
240
- colonText: {
241
- color: AppColors.grayTextWeak,
846
+ tabButtonTextActive: {
847
+ color: AppColors.purple,
848
+ fontFamily: AppFonts.interBold,
242
849
  },
243
- valText: {
244
- fontFamily: AppFonts.interRegular,
245
- fontSize: 11.5,
850
+ scrollContent: {
851
+ paddingBottom: 24,
852
+ paddingHorizontal: 12,
853
+ },
854
+ emptyState: {
855
+ padding: 24,
856
+ alignItems: 'center',
246
857
  },
247
- summaryText: {
858
+ emptyStateText: {
248
859
  fontFamily: AppFonts.interRegular,
249
- fontSize: 11,
860
+ fontSize: 12,
250
861
  color: AppColors.grayTextWeak,
862
+ textAlign: 'center',
251
863
  },
252
- });
253
- const styles = StyleSheet.create({
254
- container: {
255
- flex: 1,
256
- },
257
- storeHeader: {
864
+ listRow: {
258
865
  flexDirection: 'row',
259
866
  alignItems: 'center',
260
- backgroundColor: AppColors.purple,
261
- borderRadius: 8,
262
- paddingVertical: 10,
867
+ backgroundColor: AppColors.primaryLight,
263
868
  paddingHorizontal: 12,
264
- gap: 8,
869
+ paddingVertical: 14,
870
+ borderRadius: 10,
871
+ borderWidth: 1,
872
+ borderColor: AppColors.grayBorderSecondary,
873
+ marginBottom: 8,
874
+ shadowColor: AppColors.shadowColorString,
875
+ shadowOffset: { width: 0, height: 1 },
876
+ shadowOpacity: 0.03,
877
+ shadowRadius: 2,
878
+ elevation: 1,
265
879
  },
266
- chevronWrap: {
267
- width: 12,
268
- height: 12,
269
- alignItems: 'center',
270
- justifyContent: 'center',
880
+ rowMain: {
881
+ flex: 1,
882
+ marginLeft: 10,
271
883
  },
272
- storeTitle: {
884
+ rowTitle: {
273
885
  fontFamily: AppFonts.interBold,
274
- fontSize: 14,
275
- color: '#FFFFFF',
886
+ fontSize: 13.5,
887
+ color: AppColors.primaryBlack,
276
888
  },
277
- badge: {
278
- backgroundColor: 'rgba(255,255,255,0.2)',
279
- paddingHorizontal: 8,
889
+ rowSub: {
890
+ fontFamily: AppFonts.interRegular,
891
+ fontSize: 10,
892
+ color: AppColors.grayTextWeak,
893
+ marginTop: 2,
894
+ },
895
+ sliceIcon: {
896
+ width: 22,
897
+ height: 22,
898
+ borderRadius: 6,
899
+ backgroundColor: AppColors.purpleShade50,
900
+ alignItems: 'center',
901
+ justifyContent: 'center',
902
+ },
903
+ seqBadge: {
904
+ backgroundColor: AppColors.grayBackground,
905
+ borderColor: AppColors.grayBorderSecondary,
906
+ borderWidth: 1,
907
+ borderRadius: 4,
908
+ paddingHorizontal: 5,
280
909
  paddingVertical: 2,
281
- borderRadius: 12,
282
- marginLeft: 'auto',
910
+ alignItems: 'center',
911
+ justifyContent: 'center',
912
+ minWidth: 28,
283
913
  },
284
- badgeText: {
914
+ seqText: {
285
915
  fontFamily: AppFonts.interBold,
286
- fontSize: 10,
287
- color: '#FFFFFF',
916
+ fontSize: 9.5,
917
+ color: AppColors.grayText,
918
+ },
919
+ actionTitleText: {
920
+ fontFamily: AppFonts.interBold,
921
+ fontSize: 13,
922
+ color: AppColors.purple,
923
+ },
924
+ actionMetaRow: {
925
+ flexDirection: 'row',
926
+ alignItems: 'center',
927
+ gap: 4,
928
+ marginTop: 2,
929
+ },
930
+ actionTimeText: {
931
+ fontFamily: AppFonts.interRegular,
932
+ fontSize: 9.5,
933
+ color: AppColors.grayTextWeak,
288
934
  },
289
- storeChildren: {
290
- paddingLeft: 12,
935
+ tagsContainer: {
936
+ flexDirection: 'row',
937
+ flexWrap: 'wrap',
938
+ gap: 4,
291
939
  marginTop: 4,
292
940
  },
293
- reducerContainer: {
294
- position: 'relative',
295
- paddingLeft: 16,
296
- paddingVertical: 4,
941
+ sliceTagMini: {
942
+ backgroundColor: AppColors.purpleShade50,
943
+ borderColor: AppColors.purple + '20',
944
+ borderWidth: 1,
945
+ borderRadius: 3,
946
+ paddingHorizontal: 4,
947
+ paddingVertical: 0.5,
297
948
  },
298
- reducerVerticalLine: {
299
- position: 'absolute',
300
- left: 0,
301
- top: 0,
302
- bottom: 0,
303
- width: 1,
304
- backgroundColor: AppColors.dividerColor,
949
+ sliceTagMiniText: {
950
+ fontFamily: AppFonts.interMedium,
951
+ fontSize: 8.5,
952
+ color: AppColors.purple,
305
953
  },
306
- reducerHorizontalLine: {
307
- position: 'absolute',
308
- left: -16,
309
- top: '50%',
310
- width: 16,
311
- height: 1,
312
- backgroundColor: AppColors.dividerColor,
954
+ // ── Detail Views Styles ──
955
+ detailHeaderBar: {
956
+ flexDirection: 'row',
957
+ alignItems: 'center',
958
+ paddingHorizontal: 12,
959
+ paddingVertical: 12,
960
+ backgroundColor: AppColors.primaryLight,
961
+ borderBottomWidth: 1,
962
+ borderBottomColor: AppColors.dividerColor,
313
963
  },
314
- reducerHeader: {
964
+ backBtn: {
315
965
  flexDirection: 'row',
316
966
  alignItems: 'center',
317
967
  paddingVertical: 6,
968
+ paddingRight: 8,
969
+ },
970
+ backBtnText: {
971
+ fontFamily: AppFonts.interBold,
972
+ fontSize: 12.5,
973
+ color: AppColors.purple,
974
+ },
975
+ detailHeaderInfo: {
976
+ flex: 1,
977
+ marginLeft: 10,
978
+ marginRight: 10,
979
+ },
980
+ detailTitle: {
981
+ fontFamily: AppFonts.interBold,
982
+ fontSize: 13.5,
983
+ color: AppColors.primaryBlack,
984
+ },
985
+ detailSub: {
986
+ fontFamily: AppFonts.interRegular,
987
+ fontSize: 10,
988
+ color: AppColors.grayTextWeak,
989
+ marginTop: 2,
990
+ },
991
+ detailHeaderActions: {
992
+ flexDirection: 'row',
993
+ alignItems: 'center',
318
994
  gap: 6,
319
995
  },
320
- iconWrap: {
321
- width: 18,
322
- height: 18,
323
- borderRadius: 4,
996
+ detailHeaderBtn: {
997
+ padding: 6,
998
+ borderRadius: 6,
999
+ borderWidth: 1,
1000
+ borderColor: AppColors.purple + '20',
324
1001
  backgroundColor: AppColors.purpleShade50,
325
1002
  alignItems: 'center',
326
1003
  justifyContent: 'center',
1004
+ width: 28,
1005
+ height: 28,
1006
+ },
1007
+ detailCopyBtn: {
1008
+ padding: 6,
1009
+ borderRadius: 6,
1010
+ borderWidth: 1,
1011
+ borderColor: AppColors.purple + '20',
1012
+ backgroundColor: AppColors.purpleShade50,
1013
+ },
1014
+ detailTimeRow: {
1015
+ flexDirection: 'row',
1016
+ alignItems: 'center',
1017
+ gap: 4,
1018
+ marginTop: 1,
1019
+ },
1020
+ detailTimeText: {
1021
+ fontFamily: AppFonts.interRegular,
1022
+ fontSize: 10,
1023
+ color: AppColors.grayTextWeak,
327
1024
  },
328
- reducerText: {
1025
+ detailAffectedTags: {
1026
+ paddingHorizontal: 12,
1027
+ paddingVertical: 8,
1028
+ backgroundColor: AppColors.grayBackground,
1029
+ borderBottomWidth: 1,
1030
+ borderBottomColor: AppColors.dividerColor,
1031
+ },
1032
+ affectedTagsLabel: {
329
1033
  fontFamily: AppFonts.interBold,
330
- fontSize: 13,
331
- color: AppColors.primaryBlack,
1034
+ fontSize: 9,
1035
+ color: AppColors.grayTextWeak,
1036
+ letterSpacing: 0.5,
1037
+ marginBottom: 4,
332
1038
  },
333
- reducerChildren: {
334
- paddingLeft: 20,
335
- position: 'relative',
336
- marginTop: 4,
337
- gap: 10,
338
- },
339
- childrenVerticalLine: {
340
- position: 'absolute',
341
- left: 8,
342
- top: 0,
343
- bottom: 16,
344
- width: 1,
345
- backgroundColor: AppColors.dividerColor,
1039
+ detailTagsList: {
1040
+ flexDirection: 'row',
1041
+ flexWrap: 'wrap',
1042
+ gap: 4,
346
1043
  },
347
- actionTypeBadge: {
348
- backgroundColor: '#FCE7F3',
349
- borderColor: '#FBCFE8',
1044
+ sliceTag: {
1045
+ backgroundColor: AppColors.purpleShade50,
1046
+ borderColor: AppColors.purple + '20',
350
1047
  borderWidth: 1,
351
- borderRadius: 6,
1048
+ borderRadius: 4,
352
1049
  paddingHorizontal: 6,
353
- paddingVertical: 2,
354
- alignSelf: 'flex-start',
1050
+ paddingVertical: 1.5,
1051
+ },
1052
+ sliceTagText: {
1053
+ fontFamily: AppFonts.interMedium,
1054
+ fontSize: 9.5,
1055
+ color: AppColors.purple,
1056
+ },
1057
+ subTabRow: {
1058
+ flexDirection: 'row',
1059
+ padding: 8,
1060
+ gap: 8,
1061
+ backgroundColor: AppColors.primaryLight,
1062
+ borderBottomWidth: 1,
1063
+ borderBottomColor: AppColors.dividerColor,
1064
+ },
1065
+ subTabPill: {
1066
+ paddingHorizontal: 12,
1067
+ paddingVertical: 5,
1068
+ borderRadius: 12,
1069
+ backgroundColor: AppColors.grayBackground,
1070
+ borderWidth: 1,
1071
+ borderColor: AppColors.grayBorderSecondary,
1072
+ },
1073
+ subTabPillActive: {
1074
+ backgroundColor: AppColors.purple,
1075
+ borderColor: AppColors.purple,
1076
+ },
1077
+ subTabPillText: {
1078
+ fontFamily: AppFonts.interMedium,
1079
+ fontSize: 10.5,
1080
+ color: AppColors.grayText,
1081
+ },
1082
+ subTabPillTextActive: {
1083
+ color: '#FFFFFF',
1084
+ fontFamily: AppFonts.interBold,
355
1085
  },
356
- actionTypeText: {
1086
+ detailScrollContent: {
1087
+ paddingBottom: 24,
1088
+ },
1089
+ jsonViewerContainer: {
1090
+ paddingHorizontal: 12,
1091
+ paddingVertical: 8,
1092
+ },
1093
+ detailBox: {
1094
+ padding: 10,
1095
+ },
1096
+ detailBoxHeader: {
1097
+ flexDirection: 'row',
1098
+ alignItems: 'center',
1099
+ justifyContent: 'space-between',
1100
+ marginBottom: 8,
1101
+ paddingHorizontal: 4,
1102
+ },
1103
+ detailBoxTitle: {
357
1104
  fontFamily: AppFonts.interBold,
358
1105
  fontSize: 10,
359
- color: '#BE185D',
1106
+ color: AppColors.grayTextWeak,
1107
+ letterSpacing: 0.5,
1108
+ textTransform: 'uppercase',
1109
+ },
1110
+ apiLikeInfoBar: {
1111
+ paddingHorizontal: 16,
1112
+ paddingVertical: 14,
1113
+ backgroundColor: AppColors.primaryLight,
1114
+ borderBottomWidth: 1,
1115
+ borderBottomColor: AppColors.dividerColor,
1116
+ },
1117
+ apiBadgeRow: {
1118
+ flexDirection: 'row',
1119
+ alignItems: 'center',
1120
+ gap: 6,
1121
+ marginBottom: 8,
1122
+ },
1123
+ apiMethodBadge: {
1124
+ paddingHorizontal: 8,
1125
+ paddingVertical: 2,
1126
+ borderRadius: 4,
1127
+ alignItems: 'center',
1128
+ justifyContent: 'center',
1129
+ },
1130
+ apiMethodBadgeText: {
1131
+ fontFamily: AppFonts.interBold,
1132
+ fontSize: 9.5,
1133
+ color: '#FFFFFF',
1134
+ letterSpacing: 0.5,
1135
+ },
1136
+ apiChip: {
1137
+ paddingHorizontal: 7,
1138
+ paddingVertical: 2,
1139
+ borderRadius: 4,
1140
+ backgroundColor: 'rgba(104,75,155,0.06)',
1141
+ borderWidth: 1,
1142
+ borderColor: 'rgba(104,75,155,0.15)',
1143
+ alignItems: 'center',
1144
+ justifyContent: 'center',
1145
+ },
1146
+ apiChipText: {
1147
+ fontFamily: AppFonts.interBold,
1148
+ fontSize: 9.5,
1149
+ color: AppColors.purple,
1150
+ },
1151
+ apiTitleText: {
1152
+ fontFamily: AppFonts.interBold,
1153
+ fontSize: 16,
1154
+ color: AppColors.primaryBlack,
1155
+ marginBottom: 8,
1156
+ },
1157
+ apiMetaRow: {
1158
+ flexDirection: 'row',
1159
+ alignItems: 'center',
1160
+ gap: 4,
1161
+ },
1162
+ apiMetaText: {
1163
+ fontFamily: AppFonts.interRegular,
1164
+ fontSize: 11,
1165
+ color: AppColors.grayTextWeak,
1166
+ },
1167
+ contentDetailSubHeader: {
1168
+ flexDirection: 'row',
1169
+ alignItems: 'center',
1170
+ justifyContent: 'space-between',
1171
+ paddingHorizontal: 12,
1172
+ paddingVertical: 10,
1173
+ borderBottomWidth: 1,
1174
+ borderBottomColor: AppColors.dividerColor,
1175
+ backgroundColor: AppColors.primaryLight,
360
1176
  },
361
1177
  });