import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, FlatList, TouchableOpacity, TextInput, ScrollView, StyleSheet, } from 'react-native'; import type { PluginComponentProps, TimelineEvent, DebuggerPlugin } from '../../core/types'; import { timelineStore } from './timelineStore'; import { formatTimestamp, safeStringify, copyToClipboard } from '../../core/utils'; const CATEGORY_COLORS: Record = { navigation: '#5B8DEF', network: '#FF9500', state: '#AF52DE', user: '#34C759', lifecycle: '#FF3B30', custom: '#8E8E93', }; const TimelinePanel: React.FC = ({ theme }) => { const [events, setEvents] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [categoryFilter, setCategoryFilter] = useState(null); const [expandedId, setExpandedId] = useState(null); useEffect(() => { const unsub = timelineStore.subscribe(setEvents); return unsub; }, []); const categories = useMemo(() => { const cats = new Set(); for (const e of events) cats.add(e.category); return Array.from(cats); }, [events]); const filteredEvents = useMemo(() => { let filtered = events; if (categoryFilter) { filtered = filtered.filter((e) => e.category === categoryFilter); } if (searchQuery) { const q = searchQuery.toLowerCase(); filtered = filtered.filter( (e) => e.title.toLowerCase().includes(q) || e.category.toLowerCase().includes(q), ); } return filtered; }, [events, categoryFilter, searchQuery]); const handleClear = useCallback(() => { timelineStore.clear(); setExpandedId(null); }, []); return ( {/* Category Filter */} setCategoryFilter(null)} activeOpacity={0.7} > All ({events.length}) {categories.map((cat) => ( setCategoryFilter(categoryFilter === cat ? null : cat)} activeOpacity={0.7} > {cat} ))} Clear {/* Search */} {/* Event List */} item.id} renderItem={({ item, index }) => { const catColor = CATEGORY_COLORS[item.category] || theme.textMuted; const timeDiff = index < filteredEvents.length - 1 ? item.timestamp - filteredEvents[index + 1].timestamp : 0; return ( setExpandedId(expandedId === item.id ? null : item.id)} activeOpacity={0.7} > {index < filteredEvents.length - 1 && ( )} {item.category} {formatTimestamp(item.timestamp)} {item.title} {timeDiff > 0 && ( +{timeDiff >= 1000 ? `${(timeDiff / 1000).toFixed(1)}s` : `${timeDiff}ms`} )} {expandedId === item.id && item.data !== undefined && item.data !== null && ( copyToClipboard(safeStringify(item.data))} activeOpacity={0.7} > {String(safeStringify(item.data))} )} ); }} ListEmptyComponent={ No events logged } initialNumToRender={30} /> ); }; const styles = StyleSheet.create({ container: { flex: 1 }, filterBar: { maxHeight: 44 }, filterContent: { paddingHorizontal: 12, paddingVertical: 8, gap: 6 }, filterChip: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12, borderWidth: 1 }, filterText: { fontSize: 11, fontWeight: '600' }, searchContainer: { paddingHorizontal: 12, paddingBottom: 8 }, searchInput: { height: 36, borderRadius: 8, paddingHorizontal: 12, fontSize: 13, borderWidth: 1 }, eventRow: { flexDirection: 'row', borderBottomWidth: StyleSheet.hairlineWidth }, timelineTrack: { width: 30, alignItems: 'center', paddingTop: 14 }, timelineDot: { width: 10, height: 10, borderRadius: 5, zIndex: 1 }, timelineLine: { width: 2, flex: 1, marginTop: -2 }, eventContent: { flex: 1, paddingVertical: 10, paddingRight: 12 }, eventHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, }, catBadge: { paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4 }, catText: { fontSize: 9, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5 }, eventTime: { fontSize: 10 }, eventTitle: { fontSize: 13, fontWeight: '500' }, timeDiff: { fontSize: 10, marginTop: 2 }, dataBlock: { padding: 8, borderRadius: 6, marginTop: 8 }, dataText: { fontSize: 11, fontFamily: 'monospace', lineHeight: 16 }, emptyContainer: { alignItems: 'center', paddingVertical: 40 }, emptyText: { fontSize: 13 }, }); export function createTimelinePlugin(): DebuggerPlugin { return { id: 'timeline', name: 'Timeline', icon: '📊', component: TimelinePanel, order: 130, }; }