react-native-inapp-inspector 1.1.11 → 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 (53) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +93 -479
  2. package/dist/commonjs/components/AnalyticsEventCard.js +25 -25
  3. package/dist/commonjs/components/ConsoleLogCard.js +3 -1
  4. package/dist/commonjs/components/HighlightText.js +1 -1
  5. package/dist/commonjs/components/JsonViewer.d.ts +3 -1
  6. package/dist/commonjs/components/JsonViewer.js +327 -9
  7. package/dist/commonjs/components/MetaAccordion.d.ts +10 -1
  8. package/dist/commonjs/components/MetaAccordion.js +121 -24
  9. package/dist/commonjs/components/NetworkIcons.d.ts +1 -0
  10. package/dist/commonjs/components/NetworkIcons.js +8 -1
  11. package/dist/commonjs/components/ReduxTreeView.d.ts +12 -17
  12. package/dist/commonjs/components/ReduxTreeView.js +1045 -632
  13. package/dist/commonjs/components/SectionHeader.d.ts +1 -1
  14. package/dist/commonjs/components/SectionHeader.js +7 -1
  15. package/dist/commonjs/components/TouchableScale.js +8 -0
  16. package/dist/commonjs/constants/version.d.ts +1 -1
  17. package/dist/commonjs/constants/version.js +1 -1
  18. package/dist/commonjs/customHooks/reduxLogger.d.ts +2 -0
  19. package/dist/commonjs/customHooks/reduxLogger.js +2 -0
  20. package/dist/commonjs/helpers/index.d.ts +3 -0
  21. package/dist/commonjs/helpers/index.js +103 -1
  22. package/dist/commonjs/helpers/settingsStore.d.ts +1 -0
  23. package/dist/commonjs/helpers/settingsStore.js +4 -0
  24. package/dist/commonjs/index.js +385 -339
  25. package/dist/commonjs/types/index.d.ts +3 -0
  26. package/dist/esm/components/AnalyticsDetail.js +94 -480
  27. package/dist/esm/components/AnalyticsEventCard.js +25 -25
  28. package/dist/esm/components/ConsoleLogCard.js +3 -1
  29. package/dist/esm/components/HighlightText.js +3 -3
  30. package/dist/esm/components/JsonViewer.d.ts +3 -1
  31. package/dist/esm/components/JsonViewer.js +295 -10
  32. package/dist/esm/components/MetaAccordion.d.ts +10 -1
  33. package/dist/esm/components/MetaAccordion.js +90 -26
  34. package/dist/esm/components/NetworkIcons.d.ts +1 -0
  35. package/dist/esm/components/NetworkIcons.js +6 -0
  36. package/dist/esm/components/ReduxTreeView.d.ts +12 -17
  37. package/dist/esm/components/ReduxTreeView.js +1047 -629
  38. package/dist/esm/components/SectionHeader.d.ts +1 -1
  39. package/dist/esm/components/SectionHeader.js +8 -2
  40. package/dist/esm/components/TouchableScale.js +9 -1
  41. package/dist/esm/constants/version.d.ts +1 -1
  42. package/dist/esm/constants/version.js +1 -1
  43. package/dist/esm/customHooks/reduxLogger.d.ts +2 -0
  44. package/dist/esm/customHooks/reduxLogger.js +2 -0
  45. package/dist/esm/helpers/index.d.ts +3 -0
  46. package/dist/esm/helpers/index.js +100 -1
  47. package/dist/esm/helpers/settingsStore.d.ts +1 -0
  48. package/dist/esm/helpers/settingsStore.js +3 -0
  49. package/dist/esm/index.js +389 -343
  50. package/dist/esm/types/index.d.ts +3 -0
  51. package/example/package-lock.json +33 -74
  52. package/example/package.json +1 -1
  53. package/package.json +1 -1
@@ -1,759 +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, CopyIcon, CheckIcon } from './NetworkIcons';
6
- import Svg, { Path } from 'react-native-svg';
7
- import AnimatedEntrance from './AnimatedEntrance';
8
- import { copyToClipboard } from '../helpers';
9
- // #15 copy-to-clipboard control for a single dispatched action
10
- const ActionCopyButton = ({ value }) => {
11
- const [copied, setCopied] = useState(false);
12
- return (<Pressable hitSlop={10} onPress={() => {
13
- copyToClipboard(value(), 'Action');
14
- setCopied(true);
15
- setTimeout(() => setCopied(false), 1200);
16
- }} style={{
17
- width: 26,
18
- height: 26,
19
- borderRadius: 7,
20
- alignItems: 'center',
21
- justifyContent: 'center',
22
- backgroundColor: copied
23
- ? `${AppColors.greenColor}1A`
24
- : AppColors.grayBackground,
25
- borderWidth: 1,
26
- borderColor: AppColors.dividerColor,
27
- }}>
28
- {copied ? (<CheckIcon color={AppColors.greenColor} size={13}/>) : (<CopyIcon color={AppColors.grayTextWeak} size={13}/>)}
29
- </Pressable>);
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);
30
17
  };
31
- // #15 derive the redux module / folder name from an action type.
32
- // Supports RTK slice convention ("booking/setDate" -> "Booking") and
33
- // falls back to the first affected slice.
34
- const getActionModule = (type, affectedSlices) => {
35
- if (type && type.includes('/')) {
36
- const prefix = type.split('/')[0];
37
- if (prefix)
38
- return prefix.charAt(0).toUpperCase() + prefix.slice(1);
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);
39
35
  }
40
- if (affectedSlices && affectedSlices.length > 0) {
41
- const s = affectedSlices[0];
42
- return s.charAt(0).toUpperCase() + s.slice(1);
36
+ if (Array.isArray(val)) {
37
+ return val.some(item => valueContainsTerm(item, term));
43
38
  }
44
- return null;
45
- };
46
- // Custom icons
47
- const DatabaseIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
48
- <Path d="M12 2C6.5 2 2 4.2 2 7v10c0 2.8 4.5 5 10 5s10-2.2 10-5V7c0-2.8-4.5-5-10-5z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
49
- <Path d="M2 12c0 2.8 4.5 5 10 5s10-2.2 10-5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
50
- <Path d="M2 7c0 2.8 4.5 5 10 5s10-2.2 10-5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
51
- </Svg>);
52
- const BoltIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
53
- <Path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill={color} stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
54
- </Svg>);
55
- const FolderIcon = ({ color = AppColors.grayTextWeak, size = 12 }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
56
- <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"/>
57
- </Svg>);
58
- // Exported icons so the Redux tab's section headers and sub-tab buttons can
59
- // render real vector icons instead of emoji (which fail to render on some
60
- // Android/font setups).
61
- export const ReduxStoreIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
62
- <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"/>
63
- </Svg>);
64
- export const ReduxBoltIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
65
- <Path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill={color} stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
66
- </Svg>);
67
- // Action Timeline — a history/clock icon reads as "sequence over time".
68
- export const ReduxTimelineIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
69
- <Path d="M3 3v5h5" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
70
- <Path d="M3.05 13A9 9 0 1 0 6 5.3L3 8" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
71
- <Path d="M12 7v5l3 2" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
72
- </Svg>);
73
- // Store Tree — a node hierarchy icon reads as "nested tree structure".
74
- export const ReduxTreeIcon = ({ color = AppColors.grayTextWeak, size = 12, }) => (<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
75
- <Path d="M9 4h6v4H9zM3 16h6v4H3zm12 0h6v4h-6z" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
76
- <Path d="M12 8v4M6 16v-2h12v2" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
77
- </Svg>);
78
- const animateTreeLayout = () => {
79
- LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
39
+ return Object.keys(val).some(key => key.toLowerCase().includes(t) || valueContainsTerm(val[key], term));
80
40
  };
81
- const AnimatedChevron = ({ color, expanded, size, style, }) => {
82
- const progress = useRef(new Animated.Value(expanded ? 1 : 0)).current;
83
- useEffect(() => {
84
- Animated.timing(progress, {
85
- toValue: expanded ? 1 : 0,
86
- duration: 180,
87
- useNativeDriver: true,
88
- }).start();
89
- }, [expanded, progress]);
90
- const rotate = progress.interpolate({
91
- inputRange: [0, 1],
92
- outputRange: ['0deg', '90deg'],
93
- });
94
- return (<Animated.View style={[style, { transform: [{ rotate }] }]}>
95
- <ChevronIcon color={color} size={size}/>
96
- </Animated.View>);
97
- };
98
- const ReduxValueNode = ({ name, value, level, search }) => {
99
- const [expanded, setExpanded] = useState(level < 1);
100
- const isObject = typeof value === 'object' && value !== null;
101
- const isArray = Array.isArray(value);
102
- const nameStr = String(name);
103
- // Filter check if search query matches key or value
104
- const matchesSearch = (k, val) => {
105
- if (!search)
106
- return true;
107
- const s = search.toLowerCase();
108
- if (k.toLowerCase().includes(s))
109
- return true;
110
- if (typeof val !== 'object' && String(val).toLowerCase().includes(s))
111
- return true;
112
- if (typeof val === 'object' && val !== null) {
113
- return Object.keys(val).some(key => matchesSearch(key, val[key]));
114
- }
115
- return false;
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);
116
54
  };
117
- if (!matchesSearch(nameStr, value)) {
118
- return null;
119
- }
120
- if (!isObject) {
121
- const valStr = value === null
122
- ? 'null'
123
- : value === undefined
124
- ? 'undefined'
125
- : String(value);
126
- // Pick different colors for primitives
127
- let valColor = '#0D9488'; // String
128
- if (value === null || value === undefined) {
129
- valColor = AppColors.grayTextWeak;
130
- }
131
- else if (typeof value === 'number') {
132
- valColor = '#D97706';
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);
73
+ useEffect(() => {
74
+ if (activeSliceKey === null) {
75
+ setIsPersisted(false);
76
+ setPersistedData(null);
77
+ setSliceDetailTab('live');
78
+ return;
133
79
  }
134
- else if (typeof value === 'boolean') {
135
- valColor = '#4F46E5';
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);
136
122
  }
137
- return (<View style={[reduxValueStyles.treeRow, { paddingLeft: 12 }]}>
138
- <View style={reduxValueStyles.treeLeafConnector}/>
139
- <Text style={reduxValueStyles.keyText} selectable={true}>
140
- {nameStr}
141
- <Text style={reduxValueStyles.colonText}>: </Text>
142
- <Text style={[reduxValueStyles.valText, { color: valColor }]} selectable={true}>
143
- {valStr}
144
- </Text>
145
- </Text>
146
- </View>);
147
- }
148
- const keys = Object.keys(value);
149
- const summaryText = isArray
150
- ? `Array [${keys.length}]`
151
- : `Object {${keys.length}}`;
152
- return (<View style={reduxValueStyles.treeNodeBlock}>
153
- <Pressable onPress={() => {
154
- animateTreeLayout();
155
- setExpanded(!expanded);
156
- }} style={reduxValueStyles.treeRow}>
157
- <View style={reduxValueStyles.treeLeafConnector}/>
158
- <AnimatedChevron color={AppColors.grayTextWeak} expanded={expanded} size={10} style={reduxValueStyles.chevronWrap}/>
159
- <Text style={reduxValueStyles.keyText} selectable={true}>
160
- {nameStr}
161
- <Text style={reduxValueStyles.colonText}>: </Text>
162
- <Text style={reduxValueStyles.summaryText}>{summaryText}</Text>
163
- </Text>
164
- </Pressable>
165
- {expanded && (<View style={reduxValueStyles.treeChildrenContainer}>
166
- {keys.map(key => (<ReduxValueNode key={key} name={key} value={value[key]} level={level + 1} search={search}/>))}
167
- </View>)}
168
- </View>);
169
- };
170
- export const ReduxTreeView = ({ state, lastActionMap, search, }) => {
171
- const [storeExpanded, setStoreExpanded] = useState(true);
172
- const [reducerExpanded, setReducerExpanded] = useState({});
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;
173
170
  useEffect(() => {
174
- if (Platform.OS === 'android') {
175
- UIManager.setLayoutAnimationEnabledExperimental?.(true);
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
+ });
176
207
  }
177
- }, []);
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
+ });
224
+ }
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]);
178
236
  if (!state || typeof state !== 'object') {
179
- return (<Text style={{
180
- fontFamily: AppFonts.interRegular,
181
- fontSize: 12,
182
- color: AppColors.grayTextWeak,
183
- padding: 12,
184
- }}>
185
- No state object to display.
186
- </Text>);
237
+ return (<View style={styles.errorContainer}>
238
+ <Text style={styles.errorText}>No connected Redux store state found.</Text>
239
+ </View>);
187
240
  }
188
- const reducers = Object.keys(state);
189
- const toggleReducer = (key) => {
190
- animateTreeLayout();
191
- setReducerExpanded(prev => ({
192
- ...prev,
193
- [key]: !prev[key],
194
- }));
195
- };
196
- return (<View style={styles.container}>
197
- {/* Root Node: Store */}
198
- <Pressable onPress={() => {
199
- animateTreeLayout();
200
- setStoreExpanded(!storeExpanded);
201
- }} style={styles.storeHeader}>
202
- <AnimatedChevron color="#FFFFFF" expanded={storeExpanded} size={12} style={styles.chevronWrap}/>
203
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
204
- <ReduxStoreIcon color="#FFFFFF" size={14}/>
205
- <Text style={styles.storeTitle}>Redux Store</Text>
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'}
275
+ </Text>
276
+ </View>
277
+ </View>
206
278
  </View>
207
- <View style={styles.badge}>
208
- <Text style={styles.badgeText}>{reducers.length} Reducers</Text>
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
+ })}
209
328
  </View>
210
- </Pressable>
211
329
 
212
- {storeExpanded && (<View style={styles.storeChildren}>
213
- {reducers.map((reducerKey, index) => {
214
- const isLastReducer = index === reducers.length - 1;
215
- const isExpanded = !!reducerExpanded[reducerKey];
216
- const sliceData = state[reducerKey];
217
- const lastAction = lastActionMap[reducerKey];
218
- return (<View key={reducerKey} style={styles.reducerContainer}>
219
- {/* Visual Branch Line for Reducer */}
220
- <View style={[
221
- styles.reducerVerticalLine,
222
- isLastReducer && { bottom: '50%' },
223
- ]}/>
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>
224
338
 
225
- {/* Reducer Header */}
226
- <Pressable onPress={() => toggleReducer(reducerKey)} style={styles.reducerHeader}>
227
- <View style={styles.reducerHorizontalLine}/>
228
- <AnimatedChevron color={AppColors.purple} expanded={isExpanded} size={10} style={styles.chevronWrap}/>
229
- <View style={styles.iconWrap}>
230
- <FolderIcon color={AppColors.purple} size={11}/>
231
- </View>
232
- <Text style={styles.reducerText}>{reducerKey}</Text>
233
- </Pressable>
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>)}
234
343
 
235
- {isExpanded && (<View style={styles.reducerChildren}>
236
- {/* Vertical line connecting children */}
237
- <View style={styles.childrenVerticalLine}/>
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>
238
350
 
239
- {/* Node 1: Last Action */}
240
- <View style={styles.childItem}>
241
- <View style={styles.childHorizontalLine}/>
242
- <View style={[styles.iconWrap, { backgroundColor: '#FDF2F8' }]}>
243
- <BoltIcon color="#DB2777" size={11}/>
244
- </View>
245
- <View style={{ flex: 1 }}>
246
- <View style={{
247
- flexDirection: 'row',
248
- alignItems: 'center',
249
- flexWrap: 'wrap',
250
- gap: 6,
251
- }}>
252
- <Text style={styles.childLabel}>Last Action:</Text>
253
- {lastAction ? (<View style={styles.actionTypeBadge}>
254
- <Text style={styles.actionTypeText}>
255
- {lastAction.type}
256
- </Text>
257
- </View>) : (<Text style={styles.noActionText}>
258
- None dispatched
259
- </Text>)}
260
- </View>
261
- {lastAction && (<Text style={styles.timestampText}>
262
- Dispatched: {lastAction.timestamp}
263
- </Text>)}
264
- {lastAction && lastAction.payload !== null && (<View style={{ marginTop: 6 }}>
265
- <ReduxValueNode name="payload" value={lastAction.payload} level={0} search={search}/>
266
- </View>)}
267
- </View>
268
- </View>
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>
269
359
 
270
- {/* Node 2: State Data */}
271
- <View style={styles.childItem}>
272
- <View style={[styles.childHorizontalLine, { bottom: '50%' }]}/>
273
- <View style={[styles.iconWrap, { backgroundColor: '#ECFDF5' }]}>
274
- <DatabaseIcon color="#059669" size={11}/>
275
- </View>
276
- <View style={{ flex: 1 }}>
277
- <Text style={styles.childLabel}>State Slice Data</Text>
278
- <View style={{ marginTop: 6 }}>
279
- <ReduxValueNode name="state" value={sliceData} level={0} search={search}/>
280
- </View>
281
- </View>
282
- </View>
283
- </View>)}
284
- </View>);
285
- })}
286
- </View>)}
287
- </View>);
288
- };
289
- export const ReduxActionTimeline = ({ history, onClear, search, }) => {
290
- const [expandedActionId, setExpandedActionId] = useState(null);
291
- const toggleExpand = (id) => {
292
- animateTreeLayout();
293
- setExpandedActionId(prev => (prev === id ? null : id));
294
- };
295
- const filteredHistory = history.filter(action => {
296
- if (!search)
297
- return true;
298
- const s = search.toLowerCase();
299
- if (action.type.toLowerCase().includes(s))
300
- return true;
301
- if (action.affectedSlices.some(slice => slice.toLowerCase().includes(s)))
302
- return true;
303
- if (action.payload && typeof action.payload === 'object') {
304
- return JSON.stringify(action.payload).toLowerCase().includes(s);
305
- }
306
- return false;
307
- });
308
- return (<View style={timelineStyles.container}>
309
- <View style={timelineStyles.headerRow}>
310
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
311
- <ReduxBoltIcon color={AppColors.purple} size={15}/>
312
- <Text style={timelineStyles.headerTitle}>
313
- Dispatched Actions ({filteredHistory.length})
314
- </Text>
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>
315
365
  </View>
316
- {history.length > 0 && (<Pressable onPress={onClear} style={timelineStyles.clearBtn}>
317
- <Text style={timelineStyles.clearBtnText}>Clear Log</Text>
318
- </Pressable>)}
319
- </View>
366
+ </View>);
367
+ }
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>
320
389
 
321
- {filteredHistory.length === 0 ? (<View style={timelineStyles.emptyContainer}>
322
- <Text style={timelineStyles.emptyText}>
323
- {history.length === 0
324
- ? 'No actions dispatched yet.\nDispatch actions in your application to see the timeline.'
325
- : 'No matching actions found.'}
326
- </Text>
327
- </View>) : (<View style={timelineStyles.listContainer}>
328
- {filteredHistory.map((item, index) => {
329
- const isLast = index === filteredHistory.length - 1;
330
- const isExpanded = expandedActionId === item.id;
331
- return (<AnimatedEntrance key={item.id} index={index} distance={8} style={timelineStyles.timelineItem}>
332
- {/* Visual Line */}
333
- <View style={[
334
- timelineStyles.verticalLine,
335
- isLast && { bottom: '50%' },
336
- ]}/>
337
- <View style={timelineStyles.circleIndicator}>
338
- <View style={timelineStyles.circleInner}/>
339
- </View>
390
+ <Text style={styles.apiTitleText} selectable={true}>{selectedAction.type}</Text>
340
391
 
341
- {/* Card */}
342
- <Pressable onPress={() => toggleExpand(item.id)} style={[
343
- timelineStyles.card,
344
- isExpanded && {
345
- borderColor: AppColors.purple,
346
- backgroundColor: AppColors.purpleShade50,
347
- },
348
- ]}>
349
- <View style={timelineStyles.cardHeader}>
350
- <View style={{
351
- flexDirection: 'row',
352
- alignItems: 'center',
353
- gap: 6,
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>
397
+ </View>
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,
420
+ }}>
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={{
354
442
  flex: 1,
355
- }}>
356
- {(() => {
357
- const moduleName = getActionModule(item.type, item.affectedSlices);
358
- return moduleName ? (<View style={{
359
- paddingHorizontal: 7,
360
- paddingVertical: 2,
361
- borderRadius: 6,
362
- backgroundColor: `${AppColors.purple}14`,
363
- borderWidth: 1,
364
- borderColor: `${AppColors.purple}33`,
365
- }}>
366
- <Text style={{
367
- fontFamily: AppFonts.interBold,
368
- fontSize: 9.5,
369
- color: AppColors.purple,
370
- letterSpacing: 0.2,
371
- }}>
372
- {moduleName}
373
- </Text>
374
- </View>) : null;
375
- })()}
376
- <View style={timelineStyles.typeBadge}>
377
- <Text style={timelineStyles.typeText}>{item.type}</Text>
378
- </View>
379
- </View>
380
- <View style={{
443
+ paddingVertical: 6,
381
444
  flexDirection: 'row',
382
445
  alignItems: 'center',
383
- gap: 8,
446
+ justifyContent: 'center',
447
+ borderRadius: 8,
448
+ backgroundColor: isActive ? AppColors.purple : 'transparent',
449
+ gap: 4,
384
450
  }}>
385
- <Text style={timelineStyles.timestamp}>
386
- {item.timestamp}
387
- </Text>
388
- <ActionCopyButton value={() => ({
389
- type: item.type,
390
- payload: item.payload,
391
- })}/>
392
- </View>
393
- </View>
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>
394
462
 
395
- {item.affectedSlices.length > 0 && (<View style={timelineStyles.slicesRow}>
396
- <Text style={timelineStyles.slicesLabel}>Affected:</Text>
397
- {item.affectedSlices.map(slice => (<View key={slice} style={timelineStyles.slicePill}>
398
- <Text style={timelineStyles.sliceText}>{slice}</Text>
399
- </View>))}
400
- </View>)}
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>
401
471
 
402
- {isExpanded && (<View style={timelineStyles.payloadContainer}>
403
- <Text style={timelineStyles.payloadTitle}>Payload</Text>
404
- {item.payload !== null &&
405
- typeof item.payload === 'object' ? (<ReduxValueNode name="action.payload" value={item.payload} level={0} search={search}/>) : (<Text style={timelineStyles.primitivePayload}>
406
- {item.payload === null
407
- ? 'null'
408
- : String(item.payload)}
409
- </Text>)}
410
- </View>)}
411
- </Pressable>
412
- </AnimatedEntrance>);
413
- })}
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>);
504
+ }
505
+ // ── Render List Views (Default) ──
506
+ return (<View style={styles.container}>
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>
516
+ </View>
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>)}
538
+ </View>
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>
414
579
  </View>)}
580
+
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 => {
608
+ const sliceData = state[reducerKey];
609
+ const fieldsCount = typeof sliceData === 'object' && sliceData !== null
610
+ ? Object.keys(sliceData).length
611
+ : 1;
612
+ const lastAction = lastActionMap[reducerKey];
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}>
619
+ <FolderIcon color={AppColors.purple} size={11}/>
620
+ </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>
626
+ </View>
627
+ <ForwardChevronIcon color={AppColors.grayTextWeak} size={10}/>
628
+ </TouchableScale>);
629
+ }))}
630
+ </ScrollView>)}
631
+
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>)}
415
669
  </View>);
416
670
  };
417
- const reduxValueStyles = StyleSheet.create({
418
- treeNodeBlock: {
419
- 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',
420
682
  },
421
- treeRow: {
683
+ errorText: {
684
+ fontFamily: AppFonts.interRegular,
685
+ fontSize: 12,
686
+ color: AppColors.grayTextWeak,
687
+ },
688
+ toolbar: {
422
689
  flexDirection: 'row',
423
690
  alignItems: 'center',
424
- paddingVertical: 3,
425
- gap: 4,
426
- position: 'relative',
427
- },
428
- treeLeafConnector: {
429
- position: 'absolute',
430
- left: -12,
431
- top: '50%',
432
- width: 8,
433
- height: 1,
691
+ justifyContent: 'space-between',
692
+ paddingHorizontal: 12,
693
+ paddingVertical: 10,
694
+ backgroundColor: AppColors.primaryLight,
434
695
  borderBottomWidth: 1,
435
696
  borderBottomColor: AppColors.dividerColor,
436
- opacity: 0.5,
437
697
  },
438
- treeChildrenContainer: {
439
- paddingLeft: 14,
440
- borderLeftWidth: 1,
441
- borderLeftColor: AppColors.dividerColor,
442
- marginLeft: 6,
698
+ statusSection: {
699
+ flexDirection: 'row',
700
+ alignItems: 'center',
443
701
  },
444
- chevronWrap: {
445
- width: 10,
446
- height: 10,
702
+ statusBadge: {
703
+ flexDirection: 'row',
447
704
  alignItems: 'center',
448
- justifyContent: 'center',
705
+ paddingHorizontal: 8,
706
+ paddingVertical: 3,
707
+ borderRadius: 12,
708
+ borderWidth: 1,
449
709
  },
450
- keyText: {
451
- fontFamily: AppFonts.interMedium,
452
- fontSize: 11.5,
453
- color: AppColors.grayTextStrong,
710
+ statusBadgeActive: {
711
+ backgroundColor: AppColors.greenStatus,
712
+ borderColor: AppColors.greenColor + '30',
454
713
  },
455
- colonText: {
456
- color: AppColors.grayTextWeak,
714
+ statusBadgePaused: {
715
+ backgroundColor: AppColors.grayBackground,
716
+ borderColor: AppColors.grayBorderSecondary,
457
717
  },
458
- valText: {
459
- fontFamily: AppFonts.interRegular,
460
- fontSize: 11.5,
718
+ statusDot: {
719
+ width: 6,
720
+ height: 6,
721
+ borderRadius: 3,
722
+ marginRight: 6,
461
723
  },
462
- summaryText: {
463
- fontFamily: AppFonts.interRegular,
464
- fontSize: 11,
465
- color: AppColors.grayTextWeak,
724
+ statusDotActive: {
725
+ backgroundColor: AppColors.greenBaggageText,
466
726
  },
467
- });
468
- const timelineStyles = StyleSheet.create({
469
- container: {
470
- flex: 1,
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,
471
740
  },
472
- headerRow: {
741
+ toolbarActions: {
473
742
  flexDirection: 'row',
474
743
  alignItems: 'center',
475
- justifyContent: 'space-between',
476
- marginBottom: 12,
744
+ gap: 8,
477
745
  },
478
- headerTitle: {
479
- fontFamily: AppFonts.interBold,
480
- fontSize: 14,
481
- color: AppColors.primaryBlack,
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',
482
755
  },
483
- clearBtn: {
484
- backgroundColor: 'rgba(239, 68, 68, 0.08)',
485
- borderColor: 'rgba(239, 68, 68, 0.2)',
756
+ searchContainer: {
757
+ backgroundColor: AppColors.primaryLight,
758
+ borderBottomWidth: 1,
759
+ borderBottomColor: AppColors.dividerColor,
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,
486
771
  borderWidth: 1,
487
- paddingHorizontal: 8,
488
- paddingVertical: 4,
489
- borderRadius: 6,
772
+ borderColor: AppColors.dividerColor,
773
+ height: 36,
774
+ },
775
+ filterChipRow: {
776
+ flexDirection: 'row',
777
+ alignItems: 'center',
778
+ paddingHorizontal: 12,
779
+ gap: 8,
490
780
  },
491
- clearBtnText: {
781
+ filterChipLabel: {
492
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,
493
802
  fontSize: 10,
494
- color: '#EF4444',
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,
814
+ marginLeft: 6,
815
+ padding: 0,
495
816
  },
496
- emptyContainer: {
497
- paddingVertical: 32,
817
+ tabContainer: {
818
+ flexDirection: 'row',
819
+ backgroundColor: AppColors.dividerColor,
820
+ borderRadius: 8,
821
+ padding: 3,
822
+ marginHorizontal: 12,
823
+ marginTop: 10,
824
+ marginBottom: 10,
825
+ },
826
+ tabButton: {
827
+ flex: 1,
828
+ paddingVertical: 7,
498
829
  alignItems: 'center',
499
830
  justifyContent: 'center',
831
+ borderRadius: 6,
832
+ },
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: {
842
+ fontFamily: AppFonts.interMedium,
843
+ fontSize: 12,
844
+ color: AppColors.grayText,
500
845
  },
501
- emptyText: {
846
+ tabButtonTextActive: {
847
+ color: AppColors.purple,
848
+ fontFamily: AppFonts.interBold,
849
+ },
850
+ scrollContent: {
851
+ paddingBottom: 24,
852
+ paddingHorizontal: 12,
853
+ },
854
+ emptyState: {
855
+ padding: 24,
856
+ alignItems: 'center',
857
+ },
858
+ emptyStateText: {
502
859
  fontFamily: AppFonts.interRegular,
503
860
  fontSize: 12,
504
861
  color: AppColors.grayTextWeak,
505
862
  textAlign: 'center',
506
- lineHeight: 18,
507
- },
508
- listContainer: {
509
- paddingLeft: 12,
510
- },
511
- timelineItem: {
512
- position: 'relative',
513
- paddingLeft: 20,
514
- marginBottom: 12,
515
- },
516
- verticalLine: {
517
- position: 'absolute',
518
- left: 4,
519
- top: 0,
520
- bottom: -12,
521
- width: 1,
522
- backgroundColor: AppColors.dividerColor,
523
863
  },
524
- circleIndicator: {
525
- position: 'absolute',
526
- left: 0,
527
- top: 10,
528
- width: 9,
529
- height: 9,
530
- borderRadius: 4.5,
864
+ listRow: {
865
+ flexDirection: 'row',
866
+ alignItems: 'center',
867
+ backgroundColor: AppColors.primaryLight,
868
+ paddingHorizontal: 12,
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,
879
+ },
880
+ rowMain: {
881
+ flex: 1,
882
+ marginLeft: 10,
883
+ },
884
+ rowTitle: {
885
+ fontFamily: AppFonts.interBold,
886
+ fontSize: 13.5,
887
+ color: AppColors.primaryBlack,
888
+ },
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,
531
899
  backgroundColor: AppColors.purpleShade50,
900
+ alignItems: 'center',
901
+ justifyContent: 'center',
902
+ },
903
+ seqBadge: {
904
+ backgroundColor: AppColors.grayBackground,
905
+ borderColor: AppColors.grayBorderSecondary,
532
906
  borderWidth: 1,
533
- borderColor: AppColors.purple,
907
+ borderRadius: 4,
908
+ paddingHorizontal: 5,
909
+ paddingVertical: 2,
534
910
  alignItems: 'center',
535
911
  justifyContent: 'center',
912
+ minWidth: 28,
536
913
  },
537
- circleInner: {
538
- width: 3,
539
- height: 3,
540
- borderRadius: 1.5,
541
- backgroundColor: AppColors.purple,
914
+ seqText: {
915
+ fontFamily: AppFonts.interBold,
916
+ fontSize: 9.5,
917
+ color: AppColors.grayText,
542
918
  },
543
- card: {
544
- backgroundColor: AppColors.primaryLight,
545
- borderWidth: 1,
546
- borderColor: AppColors.grayBorderSecondary,
547
- borderRadius: 8,
548
- padding: 10,
919
+ actionTitleText: {
920
+ fontFamily: AppFonts.interBold,
921
+ fontSize: 13,
922
+ color: AppColors.purple,
549
923
  },
550
- cardHeader: {
924
+ actionMetaRow: {
551
925
  flexDirection: 'row',
552
926
  alignItems: 'center',
553
- justifyContent: 'space-between',
554
- gap: 12,
927
+ gap: 4,
928
+ marginTop: 2,
555
929
  },
556
- typeBadge: {
557
- backgroundColor: 'rgba(104,75,155,0.08)',
558
- borderColor: 'rgba(104,75,155,0.18)',
930
+ actionTimeText: {
931
+ fontFamily: AppFonts.interRegular,
932
+ fontSize: 9.5,
933
+ color: AppColors.grayTextWeak,
934
+ },
935
+ tagsContainer: {
936
+ flexDirection: 'row',
937
+ flexWrap: 'wrap',
938
+ gap: 4,
939
+ marginTop: 4,
940
+ },
941
+ sliceTagMini: {
942
+ backgroundColor: AppColors.purpleShade50,
943
+ borderColor: AppColors.purple + '20',
559
944
  borderWidth: 1,
560
- borderRadius: 6,
561
- paddingHorizontal: 6,
562
- paddingVertical: 2.5,
563
- flexShrink: 1,
945
+ borderRadius: 3,
946
+ paddingHorizontal: 4,
947
+ paddingVertical: 0.5,
948
+ },
949
+ sliceTagMiniText: {
950
+ fontFamily: AppFonts.interMedium,
951
+ fontSize: 8.5,
952
+ color: AppColors.purple,
953
+ },
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,
963
+ },
964
+ backBtn: {
965
+ flexDirection: 'row',
966
+ alignItems: 'center',
967
+ paddingVertical: 6,
968
+ paddingRight: 8,
564
969
  },
565
- typeText: {
970
+ backBtnText: {
566
971
  fontFamily: AppFonts.interBold,
567
- fontSize: 11,
972
+ fontSize: 12.5,
568
973
  color: AppColors.purple,
569
974
  },
570
- timestamp: {
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: {
571
986
  fontFamily: AppFonts.interRegular,
572
987
  fontSize: 10,
573
988
  color: AppColors.grayTextWeak,
989
+ marginTop: 2,
574
990
  },
575
- slicesRow: {
991
+ detailHeaderActions: {
992
+ flexDirection: 'row',
993
+ alignItems: 'center',
994
+ gap: 6,
995
+ },
996
+ detailHeaderBtn: {
997
+ padding: 6,
998
+ borderRadius: 6,
999
+ borderWidth: 1,
1000
+ borderColor: AppColors.purple + '20',
1001
+ backgroundColor: AppColors.purpleShade50,
1002
+ alignItems: 'center',
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: {
576
1015
  flexDirection: 'row',
577
1016
  alignItems: 'center',
578
- flexWrap: 'wrap',
579
1017
  gap: 4,
580
- marginTop: 6,
1018
+ marginTop: 1,
581
1019
  },
582
- slicesLabel: {
583
- fontFamily: AppFonts.interMedium,
1020
+ detailTimeText: {
1021
+ fontFamily: AppFonts.interRegular,
584
1022
  fontSize: 10,
585
1023
  color: AppColors.grayTextWeak,
586
- marginRight: 2,
587
1024
  },
588
- slicePill: {
1025
+ detailAffectedTags: {
1026
+ paddingHorizontal: 12,
1027
+ paddingVertical: 8,
589
1028
  backgroundColor: AppColors.grayBackground,
590
- borderColor: AppColors.dividerColor,
591
- borderWidth: 1,
592
- borderRadius: 4,
593
- paddingHorizontal: 4,
594
- paddingVertical: 1,
595
- },
596
- sliceText: {
597
- fontFamily: AppFonts.interMedium,
598
- fontSize: 9,
599
- color: AppColors.grayText,
600
- },
601
- payloadContainer: {
602
- marginTop: 10,
603
- borderTopWidth: 1,
604
- borderTopColor: AppColors.dividerColor,
605
- paddingTop: 8,
1029
+ borderBottomWidth: 1,
1030
+ borderBottomColor: AppColors.dividerColor,
606
1031
  },
607
- payloadTitle: {
1032
+ affectedTagsLabel: {
608
1033
  fontFamily: AppFonts.interBold,
609
- fontSize: 10,
1034
+ fontSize: 9,
610
1035
  color: AppColors.grayTextWeak,
611
- textTransform: 'uppercase',
1036
+ letterSpacing: 0.5,
612
1037
  marginBottom: 4,
613
1038
  },
614
- primitivePayload: {
615
- fontFamily: AppFonts.interRegular,
616
- fontSize: 11,
617
- color: AppColors.grayTextStrong,
1039
+ detailTagsList: {
1040
+ flexDirection: 'row',
1041
+ flexWrap: 'wrap',
1042
+ gap: 4,
618
1043
  },
619
- });
620
- const styles = StyleSheet.create({
621
- container: {
622
- flex: 1,
1044
+ sliceTag: {
1045
+ backgroundColor: AppColors.purpleShade50,
1046
+ borderColor: AppColors.purple + '20',
1047
+ borderWidth: 1,
1048
+ borderRadius: 4,
1049
+ paddingHorizontal: 6,
1050
+ paddingVertical: 1.5,
1051
+ },
1052
+ sliceTagText: {
1053
+ fontFamily: AppFonts.interMedium,
1054
+ fontSize: 9.5,
1055
+ color: AppColors.purple,
623
1056
  },
624
- storeHeader: {
1057
+ subTabRow: {
625
1058
  flexDirection: 'row',
626
- alignItems: 'center',
627
- backgroundColor: AppColors.purple,
628
- borderRadius: 8,
629
- paddingVertical: 10,
630
- paddingHorizontal: 12,
1059
+ padding: 8,
631
1060
  gap: 8,
1061
+ backgroundColor: AppColors.primaryLight,
1062
+ borderBottomWidth: 1,
1063
+ borderBottomColor: AppColors.dividerColor,
632
1064
  },
633
- chevronWrap: {
634
- width: 12,
635
- height: 12,
636
- alignItems: 'center',
637
- justifyContent: 'center',
1065
+ subTabPill: {
1066
+ paddingHorizontal: 12,
1067
+ paddingVertical: 5,
1068
+ borderRadius: 12,
1069
+ backgroundColor: AppColors.grayBackground,
1070
+ borderWidth: 1,
1071
+ borderColor: AppColors.grayBorderSecondary,
638
1072
  },
639
- storeTitle: {
640
- fontFamily: AppFonts.interBold,
641
- fontSize: 14,
642
- color: '#FFFFFF',
1073
+ subTabPillActive: {
1074
+ backgroundColor: AppColors.purple,
1075
+ borderColor: AppColors.purple,
643
1076
  },
644
- badge: {
645
- backgroundColor: 'rgba(255,255,255,0.2)',
646
- paddingHorizontal: 8,
647
- paddingVertical: 2,
648
- borderRadius: 12,
649
- marginLeft: 'auto',
1077
+ subTabPillText: {
1078
+ fontFamily: AppFonts.interMedium,
1079
+ fontSize: 10.5,
1080
+ color: AppColors.grayText,
650
1081
  },
651
- badgeText: {
652
- fontFamily: AppFonts.interBold,
653
- fontSize: 10,
1082
+ subTabPillTextActive: {
654
1083
  color: '#FFFFFF',
1084
+ fontFamily: AppFonts.interBold,
655
1085
  },
656
- storeChildren: {
657
- paddingLeft: 12,
658
- marginTop: 4,
1086
+ detailScrollContent: {
1087
+ paddingBottom: 24,
659
1088
  },
660
- reducerContainer: {
661
- position: 'relative',
662
- paddingLeft: 16,
663
- paddingVertical: 4,
1089
+ jsonViewerContainer: {
1090
+ paddingHorizontal: 12,
1091
+ paddingVertical: 8,
664
1092
  },
665
- reducerVerticalLine: {
666
- position: 'absolute',
667
- left: 0,
668
- top: 0,
669
- bottom: 0,
670
- width: 1,
671
- backgroundColor: AppColors.dividerColor,
1093
+ detailBox: {
1094
+ padding: 10,
672
1095
  },
673
- reducerHorizontalLine: {
674
- position: 'absolute',
675
- left: -16,
676
- top: '50%',
677
- width: 16,
678
- height: 1,
679
- backgroundColor: AppColors.dividerColor,
1096
+ detailBoxHeader: {
1097
+ flexDirection: 'row',
1098
+ alignItems: 'center',
1099
+ justifyContent: 'space-between',
1100
+ marginBottom: 8,
1101
+ paddingHorizontal: 4,
680
1102
  },
681
- reducerHeader: {
1103
+ detailBoxTitle: {
1104
+ fontFamily: AppFonts.interBold,
1105
+ fontSize: 10,
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: {
682
1118
  flexDirection: 'row',
683
1119
  alignItems: 'center',
684
- paddingVertical: 6,
685
1120
  gap: 6,
1121
+ marginBottom: 8,
686
1122
  },
687
- iconWrap: {
688
- width: 18,
689
- height: 18,
1123
+ apiMethodBadge: {
1124
+ paddingHorizontal: 8,
1125
+ paddingVertical: 2,
690
1126
  borderRadius: 4,
691
- backgroundColor: AppColors.purpleShade50,
692
1127
  alignItems: 'center',
693
1128
  justifyContent: 'center',
694
1129
  },
695
- reducerText: {
1130
+ apiMethodBadgeText: {
696
1131
  fontFamily: AppFonts.interBold,
697
- fontSize: 13,
698
- color: AppColors.primaryBlack,
699
- },
700
- reducerChildren: {
701
- paddingLeft: 20,
702
- position: 'relative',
703
- marginTop: 4,
704
- gap: 10,
705
- },
706
- childrenVerticalLine: {
707
- position: 'absolute',
708
- left: 8,
709
- top: 0,
710
- bottom: 16,
711
- width: 1,
712
- backgroundColor: AppColors.dividerColor,
1132
+ fontSize: 9.5,
1133
+ color: '#FFFFFF',
1134
+ letterSpacing: 0.5,
713
1135
  },
714
- childItem: {
715
- flexDirection: 'row',
716
- alignItems: 'flex-start',
717
- gap: 8,
718
- position: 'relative',
719
- paddingLeft: 12,
720
- },
721
- childHorizontalLine: {
722
- position: 'absolute',
723
- left: -12,
724
- top: 10,
725
- width: 12,
726
- height: 1,
727
- backgroundColor: AppColors.dividerColor,
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',
728
1145
  },
729
- childLabel: {
1146
+ apiChipText: {
730
1147
  fontFamily: AppFonts.interBold,
731
- fontSize: 11.5,
732
- color: AppColors.grayText,
733
- marginTop: 2,
734
- },
735
- actionTypeBadge: {
736
- backgroundColor: '#FCE7F3',
737
- borderColor: '#FBCFE8',
738
- borderWidth: 1,
739
- borderRadius: 6,
740
- paddingHorizontal: 6,
741
- paddingVertical: 2,
1148
+ fontSize: 9.5,
1149
+ color: AppColors.purple,
742
1150
  },
743
- actionTypeText: {
1151
+ apiTitleText: {
744
1152
  fontFamily: AppFonts.interBold,
745
- fontSize: 10.5,
746
- color: '#BE185D',
1153
+ fontSize: 16,
1154
+ color: AppColors.primaryBlack,
1155
+ marginBottom: 8,
1156
+ },
1157
+ apiMetaRow: {
1158
+ flexDirection: 'row',
1159
+ alignItems: 'center',
1160
+ gap: 4,
747
1161
  },
748
- noActionText: {
1162
+ apiMetaText: {
749
1163
  fontFamily: AppFonts.interRegular,
750
1164
  fontSize: 11,
751
1165
  color: AppColors.grayTextWeak,
752
1166
  },
753
- timestampText: {
754
- fontFamily: AppFonts.interRegular,
755
- fontSize: 9.5,
756
- color: AppColors.grayTextWeak,
757
- marginTop: 2,
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,
758
1176
  },
759
1177
  });