import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, TouchableOpacity, ScrollView, TextInput, StyleSheet } from 'react-native'; import type { PluginComponentProps, DebuggerPlugin } from '../../core/types'; import { stateAdapterRegistry, type StateAdapter } from './stateAdapterRegistry'; import { safeStringify, copyToClipboard } from '../../core/utils'; // ─── JSON Tree Viewer ─────────────────────────────────────────────────────── interface JsonNodeProps { keyName: string; value: unknown; depth: number; theme: PluginComponentProps['theme']; searchQuery: string; } const JsonNode: React.FC = React.memo( ({ keyName, value, depth, theme, searchQuery }) => { const [expanded, setExpanded] = useState(depth < 2); const isObject = typeof value === 'object' && value !== null; const isArray = Array.isArray(value); const entries = isObject ? Object.entries(value as Record) : []; const typeLabel = isArray ? `Array(${entries.length})` : `Object(${entries.length})`; const matchesSearch = searchQuery && keyName.toLowerCase().includes(searchQuery.toLowerCase()); const valueDisplay = useMemo(() => { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (typeof value === 'boolean') return value ? 'true' : 'false'; if (typeof value === 'number') return String(value); if (typeof value === 'string') return `"${value.length > 100 ? value.slice(0, 100) + '…' : value}"`; return null; }, [value]); const valueColor = useMemo(() => { if (value === null || value === undefined) return theme.textMuted; if (typeof value === 'boolean') return theme.accent; if (typeof value === 'number') return theme.success; if (typeof value === 'string') return theme.warning; return theme.text; }, [value, theme]); if (!isObject) { return ( copyToClipboard(safeStringify(value, 0))} activeOpacity={0.7} > {keyName}:{' '} {valueDisplay} ); } return ( setExpanded(!expanded)} onLongPress={() => copyToClipboard(safeStringify(value))} activeOpacity={0.7} > {expanded ? '▼' : '▶'}{' '} {keyName} {typeLabel} {expanded && entries.map(([key, val]) => ( ))} ); }, ); // ─── State Inspector Panel ────────────────────────────────────────────────── const StateInspectorPanel: React.FC = ({ theme }) => { const [adapters, setAdapters] = useState>(new Map()); const [states, setStates] = useState>(new Map()); const [activeStore, setActiveStore] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [, forceUpdate] = useState(0); // Listen for adapter registry changes useEffect(() => { const unsubscribe = stateAdapterRegistry.subscribe(() => { setAdapters(stateAdapterRegistry.getAll()); }); setAdapters(stateAdapterRegistry.getAll()); return unsubscribe; }, []); // Subscribe to state changes from all adapters useEffect(() => { const unsubscribers: (() => void)[] = []; const refreshStates = () => { const newStates = new Map(); for (const [id, adapter] of adapters) { try { newStates.set(id, adapter.getState()); } catch { newStates.set(id, { error: 'Failed to read state' }); } } setStates(newStates); }; refreshStates(); for (const [id, adapter] of adapters) { if (adapter.subscribe) { const unsub = adapter.subscribe(() => { try { setStates((prev) => { const next = new Map(prev); next.set(id, adapter.getState()); return next; }); } catch { // ignore } }); unsubscribers.push(unsub); } } return () => unsubscribers.forEach((u) => u()); }, [adapters]); const handleRefresh = useCallback(() => { const newStates = new Map(); for (const [id, adapter] of adapters) { try { newStates.set(id, adapter.getState()); } catch { newStates.set(id, { error: 'Failed to read state' }); } } setStates(newStates); forceUpdate((n) => n + 1); }, [adapters]); const handleCopyState = useCallback( (storeId: string) => { const state = states.get(storeId); if (state) copyToClipboard(safeStringify(state)); }, [states], ); const storeIds = useMemo(() => Array.from(adapters.keys()), [adapters]); const activeStoreId = activeStore || storeIds[0] || null; const activeState = activeStoreId ? states.get(activeStoreId) : null; const activeAdapter = activeStoreId ? adapters.get(activeStoreId) : null; if (storeIds.length === 0) { return ( 🔍 No State Stores Registered Register your stores using:{'\n\n'} { 'setStateAdapter("redux", {\n name: "Redux Store",\n getState: () => store.getState(),\n subscribe: (cb) => store.subscribe(cb)\n})' } ); } return ( {/* Store Selector */} {storeIds.length > 1 && ( {storeIds.map((id) => { const adapter = adapters.get(id); return ( setActiveStore(id)} activeOpacity={0.7} > {adapter?.name || id} ); })} )} {/* Actions */} {activeAdapter?.name || activeStoreId} ↻ Refresh {activeStoreId && ( handleCopyState(activeStoreId)} activeOpacity={0.7} style={styles.actionBtn} > Copy )} {/* Search */} {/* State Tree */} {activeState && typeof activeState === 'object' ? ( Object.entries(activeState as Record).map(([key, val]) => ( )) ) : ( {safeStringify(activeState)} )} ); }; const styles = StyleSheet.create({ container: { flex: 1 }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, emptyIcon: { fontSize: 48, marginBottom: 16 }, emptyTitle: { fontSize: 16, fontWeight: '700', marginBottom: 8 }, emptyDesc: { fontSize: 13, textAlign: 'center', lineHeight: 20 }, storeBar: { maxHeight: 44 }, storeBarContent: { paddingHorizontal: 12, paddingVertical: 8, gap: 6 }, storeChip: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12, borderWidth: 1 }, storeChipText: { fontSize: 12, fontWeight: '600' }, actionBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, }, storeName: { fontSize: 14, fontWeight: '700' }, actionButtons: { flexDirection: 'row', gap: 12 }, actionBtn: {}, actionBtnText: { fontSize: 12, fontWeight: '600' }, searchContainer: { paddingHorizontal: 12, paddingVertical: 8 }, searchInput: { height: 36, borderRadius: 8, paddingHorizontal: 12, fontSize: 13, borderWidth: 1 }, treeContainer: { flex: 1 }, nodeRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 4, paddingRight: 12 }, nodeExpander: { fontSize: 10 }, nodeKey: { fontSize: 12, fontWeight: '600' }, nodeType: { fontSize: 10, fontStyle: 'italic' }, nodeValue: { fontSize: 12, flex: 1 }, }); export function createStateInspectorPlugin(): DebuggerPlugin { return { id: 'state-inspector', name: 'State', icon: '🔍', component: StateInspectorPanel, order: 30, }; }