@buoy-gg/route-events 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +654 -0
- package/lib/commonjs/RouteObserver.js +54 -0
- package/lib/commonjs/RouteParser.js +310 -0
- package/lib/commonjs/RouteTracker.js +39 -0
- package/lib/commonjs/components/NavigationStack.js +584 -0
- package/lib/commonjs/components/RouteEventDetailContent.js +492 -0
- package/lib/commonjs/components/RouteEventExpandedContent.js +187 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +175 -0
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +560 -0
- package/lib/commonjs/components/RouteEventsTimeline.js +82 -0
- package/lib/commonjs/components/RouteFilterViewV2.js +42 -0
- package/lib/commonjs/components/RoutesSitemap.js +948 -0
- package/lib/commonjs/expoRouterStore.js +104 -0
- package/lib/commonjs/index.js +99 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/preset.js +83 -0
- package/lib/commonjs/useNavigationStack.js +241 -0
- package/lib/commonjs/useRouteObserver.js +73 -0
- package/lib/commonjs/useRouteSitemap.js +234 -0
- package/lib/commonjs/utils/safeExpoRouter.js +129 -0
- package/lib/commonjs/utils/safeReactNavigation.js +104 -0
- package/lib/module/RouteObserver.js +49 -0
- package/lib/module/RouteParser.js +305 -0
- package/lib/module/RouteTracker.js +35 -0
- package/lib/module/components/NavigationStack.js +580 -0
- package/lib/module/components/RouteEventDetailContent.js +487 -0
- package/lib/module/components/RouteEventExpandedContent.js +183 -0
- package/lib/module/components/RouteEventItemCompact.js +171 -0
- package/lib/module/components/RouteEventsModalWithTabs.js +557 -0
- package/lib/module/components/RouteEventsTimeline.js +78 -0
- package/lib/module/components/RouteFilterViewV2.js +38 -0
- package/lib/module/components/RoutesSitemap.js +944 -0
- package/lib/module/expoRouterStore.js +98 -0
- package/lib/module/index.js +23 -0
- package/lib/module/preset.js +79 -0
- package/lib/module/useNavigationStack.js +238 -0
- package/lib/module/useRouteObserver.js +70 -0
- package/lib/module/useRouteSitemap.js +229 -0
- package/lib/module/utils/safeExpoRouter.js +120 -0
- package/lib/module/utils/safeReactNavigation.js +98 -0
- package/lib/typescript/RouteObserver.d.ts +37 -0
- package/lib/typescript/RouteObserver.d.ts.map +1 -0
- package/lib/typescript/RouteParser.d.ts +129 -0
- package/lib/typescript/RouteParser.d.ts.map +1 -0
- package/lib/typescript/RouteTracker.d.ts +29 -0
- package/lib/typescript/RouteTracker.d.ts.map +1 -0
- package/lib/typescript/components/NavigationStack.d.ts +11 -0
- package/lib/typescript/components/NavigationStack.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts +21 -0
- package/lib/typescript/components/RouteEventDetailContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts +16 -0
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts +15 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts +15 -0
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts +17 -0
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts +9 -0
- package/lib/typescript/components/RouteFilterViewV2.d.ts.map +1 -0
- package/lib/typescript/components/RoutesSitemap.d.ts +15 -0
- package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -0
- package/lib/typescript/expoRouterStore.d.ts +28 -0
- package/lib/typescript/expoRouterStore.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +18 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts +76 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/useNavigationStack.d.ts +48 -0
- package/lib/typescript/useNavigationStack.d.ts.map +1 -0
- package/lib/typescript/useRouteObserver.d.ts +27 -0
- package/lib/typescript/useRouteObserver.d.ts.map +1 -0
- package/lib/typescript/useRouteSitemap.d.ts +102 -0
- package/lib/typescript/useRouteSitemap.d.ts.map +1 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts +13 -0
- package/lib/typescript/utils/safeExpoRouter.d.ts.map +1 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts +10 -0
- package/lib/typescript/utils/safeReactNavigation.d.ts.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from "react";
|
|
4
|
+
import { Text, View, TouchableOpacity, StyleSheet, Alert } from "react-native";
|
|
5
|
+
import { useRouter } from "expo-router";
|
|
6
|
+
import { JsModal, ModalHeader, TabSelector, formatRelativeTime, devToolsStorageKeys, Navigation, Pause, Play, Trash2, Filter, SearchBar, safeGetItem, safeSetItem, ToolbarCopyButton, Lock, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
|
|
7
|
+
|
|
8
|
+
// Lazy load the license hooks to avoid circular dependencies
|
|
9
|
+
let _useIsPro = null;
|
|
10
|
+
let _licenseLoadAttempted = false;
|
|
11
|
+
function loadLicenseModule() {
|
|
12
|
+
if (_licenseLoadAttempted) return;
|
|
13
|
+
_licenseLoadAttempted = true;
|
|
14
|
+
try {
|
|
15
|
+
const mod = require("@buoy-gg/license");
|
|
16
|
+
if (mod) {
|
|
17
|
+
_useIsPro = mod.useIsPro ?? null;
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// License package not available
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function getUseIsPro() {
|
|
24
|
+
loadLicenseModule();
|
|
25
|
+
return _useIsPro ?? (() => false);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Free tier limit for route events
|
|
29
|
+
const FREE_TIER_EVENT_LIMIT = 3;
|
|
30
|
+
import { routeObserver as defaultRouteObserver } from "../RouteObserver";
|
|
31
|
+
import { RouteFilterViewV2 } from "./RouteFilterViewV2";
|
|
32
|
+
import { RoutesSitemap } from "./RoutesSitemap";
|
|
33
|
+
import { NavigationStack } from "./NavigationStack";
|
|
34
|
+
import { RouteEventsTimeline } from "./RouteEventsTimeline";
|
|
35
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
36
|
+
export function RouteEventsModalWithTabs({
|
|
37
|
+
visible,
|
|
38
|
+
onClose,
|
|
39
|
+
onBack,
|
|
40
|
+
onMinimize,
|
|
41
|
+
enableSharedModalDimensions = false,
|
|
42
|
+
routeObserver = defaultRouteObserver
|
|
43
|
+
}) {
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
const [activeTab, setActiveTab] = useState("events");
|
|
46
|
+
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Check Pro status internally
|
|
49
|
+
const useIsPro = getUseIsPro();
|
|
50
|
+
const isPro = useIsPro();
|
|
51
|
+
|
|
52
|
+
// NOTE: Route tracking requires <RouteTracker /> to be placed inside the navigation tree.
|
|
53
|
+
// The useRouteObserver hook uses expo-router hooks that only work inside Stack/Tabs/Slot.
|
|
54
|
+
// See the RouteTracker component export for easy setup.
|
|
55
|
+
|
|
56
|
+
// Event Listener state
|
|
57
|
+
const [events, setEvents] = useState([]);
|
|
58
|
+
const [isListening, setIsListening] = useState(false);
|
|
59
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
60
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
61
|
+
const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["/_sitemap", "/api", "/__dev"]));
|
|
62
|
+
const lastEventRef = useRef(null);
|
|
63
|
+
const hasLoadedFilters = useRef(false);
|
|
64
|
+
const hasLoadedTabState = useRef(false);
|
|
65
|
+
const hasLoadedMonitoringState = useRef(false);
|
|
66
|
+
const handleModeChange = useCallback(_mode => {
|
|
67
|
+
// Mode changes handled by JsModal
|
|
68
|
+
}, []);
|
|
69
|
+
const handleNavigate = useCallback(pathname => {
|
|
70
|
+
try {
|
|
71
|
+
router.push(pathname);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
Alert.alert("Navigation Error", String(error));
|
|
74
|
+
}
|
|
75
|
+
}, [router]);
|
|
76
|
+
|
|
77
|
+
// Load persisted tab state on mount
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!visible || hasLoadedTabState.current) return;
|
|
80
|
+
const loadTabState = async () => {
|
|
81
|
+
try {
|
|
82
|
+
const storedTab = await safeGetItem(devToolsStorageKeys.routeEvents.activeTab());
|
|
83
|
+
if (storedTab && (storedTab === "routes" || storedTab === "events")) {
|
|
84
|
+
setActiveTab(storedTab);
|
|
85
|
+
}
|
|
86
|
+
hasLoadedTabState.current = true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// Failed to load tab state
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
loadTabState();
|
|
92
|
+
}, [visible]);
|
|
93
|
+
|
|
94
|
+
// Load persisted monitoring state on mount
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (!visible || hasLoadedMonitoringState.current) return;
|
|
97
|
+
const loadMonitoringState = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const storedMonitoring = await safeGetItem(devToolsStorageKeys.routeEvents.isMonitoring());
|
|
100
|
+
if (storedMonitoring !== null) {
|
|
101
|
+
const shouldMonitor = storedMonitoring === "true";
|
|
102
|
+
setIsListening(shouldMonitor);
|
|
103
|
+
}
|
|
104
|
+
hasLoadedMonitoringState.current = true;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Failed to load monitoring state
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
loadMonitoringState();
|
|
110
|
+
}, [visible]);
|
|
111
|
+
|
|
112
|
+
// Save tab state when it changes
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!hasLoadedTabState.current) return;
|
|
115
|
+
const saveTabState = async () => {
|
|
116
|
+
try {
|
|
117
|
+
await safeSetItem(devToolsStorageKeys.routeEvents.activeTab(), activeTab);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Failed to save tab state
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
saveTabState();
|
|
123
|
+
}, [activeTab]);
|
|
124
|
+
|
|
125
|
+
// Save monitoring state when it changes
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!hasLoadedMonitoringState.current) return;
|
|
128
|
+
const saveMonitoringState = async () => {
|
|
129
|
+
try {
|
|
130
|
+
await safeSetItem(devToolsStorageKeys.routeEvents.isMonitoring(), isListening.toString());
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Failed to save monitoring state
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
saveMonitoringState();
|
|
136
|
+
}, [isListening]);
|
|
137
|
+
|
|
138
|
+
// Load persisted filters on mount
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!visible || hasLoadedFilters.current) return;
|
|
141
|
+
const loadFilters = async () => {
|
|
142
|
+
try {
|
|
143
|
+
const storedFilters = await safeGetItem(devToolsStorageKeys.routeEvents.eventFilters());
|
|
144
|
+
if (storedFilters) {
|
|
145
|
+
const filters = JSON.parse(storedFilters);
|
|
146
|
+
setIgnoredPatterns(new Set(filters));
|
|
147
|
+
}
|
|
148
|
+
hasLoadedFilters.current = true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Failed to load filters
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
loadFilters();
|
|
154
|
+
}, [visible]);
|
|
155
|
+
|
|
156
|
+
// Save filters when they change
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (!hasLoadedFilters.current) return;
|
|
159
|
+
const saveFilters = async () => {
|
|
160
|
+
try {
|
|
161
|
+
const filters = Array.from(ignoredPatterns);
|
|
162
|
+
await safeSetItem(devToolsStorageKeys.routeEvents.eventFilters(), JSON.stringify(filters));
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Failed to save filters
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
saveFilters();
|
|
168
|
+
}, [ignoredPatterns]);
|
|
169
|
+
|
|
170
|
+
// Event listener setup - keeps capturing even when minimized
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!isListening) return;
|
|
173
|
+
|
|
174
|
+
// Set up event listener
|
|
175
|
+
const unsubscribe = routeObserver.addListener(event => {
|
|
176
|
+
lastEventRef.current = event;
|
|
177
|
+
setEvents(prev => {
|
|
178
|
+
const updated = [event, ...prev];
|
|
179
|
+
return updated.slice(0, 500);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
return () => {
|
|
183
|
+
unsubscribe();
|
|
184
|
+
};
|
|
185
|
+
}, [isListening, routeObserver]);
|
|
186
|
+
const handleToggleListening = useCallback(() => {
|
|
187
|
+
setIsListening(prev => !prev);
|
|
188
|
+
}, []);
|
|
189
|
+
const handleClearEvents = useCallback(() => {
|
|
190
|
+
if (events.length === 0) return;
|
|
191
|
+
|
|
192
|
+
// Gate clear behind Pro
|
|
193
|
+
if (!isPro) {
|
|
194
|
+
setShowUpgradeModal(true);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
Alert.alert("Clear Events", `Clear ${events.length} event${events.length !== 1 ? "s" : ""}?`, [{
|
|
198
|
+
text: "Cancel",
|
|
199
|
+
style: "cancel"
|
|
200
|
+
}, {
|
|
201
|
+
text: "Clear",
|
|
202
|
+
style: "destructive",
|
|
203
|
+
onPress: () => setEvents([])
|
|
204
|
+
}]);
|
|
205
|
+
}, [events.length, isPro]);
|
|
206
|
+
const handleTogglePattern = useCallback(pattern => {
|
|
207
|
+
setIgnoredPatterns(prev => {
|
|
208
|
+
const next = new Set(prev);
|
|
209
|
+
if (next.has(pattern)) {
|
|
210
|
+
next.delete(pattern);
|
|
211
|
+
} else {
|
|
212
|
+
next.add(pattern);
|
|
213
|
+
}
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
}, []);
|
|
217
|
+
const handleAddPattern = useCallback(pattern => {
|
|
218
|
+
setIgnoredPatterns(prev => new Set([...prev, pattern]));
|
|
219
|
+
}, []);
|
|
220
|
+
const handleToggleFilters = useCallback(() => {
|
|
221
|
+
setShowFilters(!showFilters);
|
|
222
|
+
}, [showFilters]);
|
|
223
|
+
|
|
224
|
+
// Get all unique pathnames from events
|
|
225
|
+
const allEventPathnames = useMemo(() => {
|
|
226
|
+
const pathnames = new Set();
|
|
227
|
+
events.forEach(event => {
|
|
228
|
+
if (event.pathname) {
|
|
229
|
+
pathnames.add(event.pathname);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return Array.from(pathnames).sort();
|
|
233
|
+
}, [events]);
|
|
234
|
+
|
|
235
|
+
// Filter events based on ignored patterns and search query
|
|
236
|
+
const filteredEvents = useMemo(() => {
|
|
237
|
+
return events.filter(event => {
|
|
238
|
+
if (!event.pathname) return false;
|
|
239
|
+
|
|
240
|
+
// Filter out pathnames that match ignored patterns
|
|
241
|
+
const shouldIgnore = Array.from(ignoredPatterns).some(pattern => event.pathname.includes(pattern));
|
|
242
|
+
if (shouldIgnore) return false;
|
|
243
|
+
|
|
244
|
+
// Filter by search query (search in pathname and param values)
|
|
245
|
+
if (searchQuery.trim()) {
|
|
246
|
+
const query = searchQuery.toLowerCase();
|
|
247
|
+
const pathnameMatch = event.pathname.toLowerCase().includes(query);
|
|
248
|
+
const paramsMatch = Object.entries(event.params || {}).some(([key, value]) => {
|
|
249
|
+
const valueStr = Array.isArray(value) ? value.join(" ") : value;
|
|
250
|
+
return key.toLowerCase().includes(query) || valueStr.toLowerCase().includes(query);
|
|
251
|
+
});
|
|
252
|
+
return pathnameMatch || paramsMatch;
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
}, [events, ignoredPatterns, searchQuery]);
|
|
257
|
+
|
|
258
|
+
// Limit visible events for free tier
|
|
259
|
+
const visibleEvents = useMemo(() => {
|
|
260
|
+
if (isPro) return filteredEvents;
|
|
261
|
+
return filteredEvents.slice(0, FREE_TIER_EVENT_LIMIT);
|
|
262
|
+
}, [filteredEvents, isPro]);
|
|
263
|
+
|
|
264
|
+
// Calculate locked event count
|
|
265
|
+
const lockedEventCount = useMemo(() => {
|
|
266
|
+
if (isPro) return 0;
|
|
267
|
+
return Math.max(0, filteredEvents.length - FREE_TIER_EVENT_LIMIT);
|
|
268
|
+
}, [filteredEvents.length, isPro]);
|
|
269
|
+
|
|
270
|
+
// Calculate visit counts for each route (for duplicate detection)
|
|
271
|
+
const visitCounts = useMemo(() => {
|
|
272
|
+
const counts = new Map();
|
|
273
|
+
const eventVisitNumbers = new Map();
|
|
274
|
+
|
|
275
|
+
// Iterate from oldest to newest to assign visit numbers correctly
|
|
276
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
277
|
+
const event = events[i];
|
|
278
|
+
const currentCount = counts.get(event.pathname) || 0;
|
|
279
|
+
const visitNumber = currentCount + 1;
|
|
280
|
+
counts.set(event.pathname, visitNumber);
|
|
281
|
+
eventVisitNumbers.set(i, visitNumber);
|
|
282
|
+
}
|
|
283
|
+
return eventVisitNumbers;
|
|
284
|
+
}, [events]);
|
|
285
|
+
|
|
286
|
+
// Prepare copy data - memoized so it only rebuilds when dependencies change
|
|
287
|
+
const copyAllEventsData = useMemo(() => {
|
|
288
|
+
return {
|
|
289
|
+
summary: {
|
|
290
|
+
total: events.length,
|
|
291
|
+
filtered: filteredEvents.length,
|
|
292
|
+
listening: isListening,
|
|
293
|
+
ignoredPatterns: Array.from(ignoredPatterns),
|
|
294
|
+
timestamp: new Date().toISOString()
|
|
295
|
+
},
|
|
296
|
+
events: filteredEvents.map((event, index) => ({
|
|
297
|
+
index,
|
|
298
|
+
visitNumber: visitCounts.get(index) || 1,
|
|
299
|
+
pathname: event.pathname,
|
|
300
|
+
timestamp: event.timestamp,
|
|
301
|
+
timestampRelative: formatRelativeTime(event.timestamp),
|
|
302
|
+
params: event.params,
|
|
303
|
+
segments: event.segments,
|
|
304
|
+
previousPathname: event.previousPathname,
|
|
305
|
+
timeSincePrevious: event.timeSincePrevious
|
|
306
|
+
}))
|
|
307
|
+
};
|
|
308
|
+
}, [events.length, filteredEvents, isListening, ignoredPatterns, visitCounts]);
|
|
309
|
+
if (!visible) return null;
|
|
310
|
+
const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.routeEvents.modal();
|
|
311
|
+
|
|
312
|
+
// No footer needed since we're showing a timeline
|
|
313
|
+
const footerNode = null;
|
|
314
|
+
const renderContent = () => {
|
|
315
|
+
if (activeTab === "routes") {
|
|
316
|
+
return /*#__PURE__*/_jsx(RoutesSitemap, {
|
|
317
|
+
style: styles.contentWrapper
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (activeTab === "stack") {
|
|
321
|
+
return /*#__PURE__*/_jsx(NavigationStack, {
|
|
322
|
+
style: styles.contentWrapper
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Events tab content
|
|
327
|
+
if (showFilters) {
|
|
328
|
+
return /*#__PURE__*/_jsx(RouteFilterViewV2, {
|
|
329
|
+
ignoredPatterns: ignoredPatterns,
|
|
330
|
+
onTogglePattern: handleTogglePattern,
|
|
331
|
+
onAddPattern: handleAddPattern,
|
|
332
|
+
availablePathnames: allEventPathnames
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Show search bar if there are events (even if filtered to 0)
|
|
337
|
+
const showSearchBar = events.length > 0;
|
|
338
|
+
if (filteredEvents.length === 0) {
|
|
339
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
340
|
+
style: styles.contentWrapper,
|
|
341
|
+
children: [showSearchBar && /*#__PURE__*/_jsx(View, {
|
|
342
|
+
style: styles.searchContainer,
|
|
343
|
+
children: /*#__PURE__*/_jsx(SearchBar, {
|
|
344
|
+
value: searchQuery,
|
|
345
|
+
onChange: setSearchQuery,
|
|
346
|
+
placeholder: "Search pathname or params..."
|
|
347
|
+
})
|
|
348
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
349
|
+
style: styles.emptyState,
|
|
350
|
+
children: [/*#__PURE__*/_jsx(Navigation, {
|
|
351
|
+
size: 48,
|
|
352
|
+
color: buoyColors.textMuted
|
|
353
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
354
|
+
style: styles.emptyTitle,
|
|
355
|
+
children: searchQuery.trim() ? "No matching events" : isListening ? "No route events yet" : "Event listener is paused"
|
|
356
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
357
|
+
style: styles.emptySubtitle,
|
|
358
|
+
children: searchQuery.trim() ? "Try a different search term" : isListening ? "Navigation events will appear here" : "Press play to start monitoring"
|
|
359
|
+
})]
|
|
360
|
+
})]
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Show chronological timeline of events
|
|
365
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
366
|
+
style: styles.contentWrapper,
|
|
367
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
368
|
+
style: styles.searchContainer,
|
|
369
|
+
children: /*#__PURE__*/_jsx(SearchBar, {
|
|
370
|
+
value: searchQuery,
|
|
371
|
+
onChange: setSearchQuery,
|
|
372
|
+
placeholder: "Search pathname or params..."
|
|
373
|
+
})
|
|
374
|
+
}), lockedEventCount > 0 && /*#__PURE__*/_jsxs(View, {
|
|
375
|
+
style: styles.lockedEventsBanner,
|
|
376
|
+
children: [/*#__PURE__*/_jsx(Lock, {
|
|
377
|
+
size: 14,
|
|
378
|
+
color: buoyColors.warning
|
|
379
|
+
}), /*#__PURE__*/_jsxs(Text, {
|
|
380
|
+
style: styles.lockedEventsBannerText,
|
|
381
|
+
children: [lockedEventCount, " older ", lockedEventCount === 1 ? 'event' : 'events', " locked"]
|
|
382
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
383
|
+
style: styles.lockedEventsBannerSubtext,
|
|
384
|
+
children: "Upgrade to Pro for full history"
|
|
385
|
+
})]
|
|
386
|
+
}), /*#__PURE__*/_jsx(RouteEventsTimeline, {
|
|
387
|
+
events: visibleEvents,
|
|
388
|
+
visitCounts: visitCounts,
|
|
389
|
+
onNavigate: handleNavigate
|
|
390
|
+
})]
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
return /*#__PURE__*/_jsxs(JsModal, {
|
|
394
|
+
visible: visible,
|
|
395
|
+
onClose: onClose,
|
|
396
|
+
onMinimize: onMinimize,
|
|
397
|
+
persistenceKey: persistenceKey,
|
|
398
|
+
header: {
|
|
399
|
+
showToggleButton: true,
|
|
400
|
+
customContent: showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
|
|
401
|
+
children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
402
|
+
onBack: () => setShowFilters(false)
|
|
403
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
404
|
+
title: "Filters"
|
|
405
|
+
})]
|
|
406
|
+
}) : /*#__PURE__*/_jsxs(ModalHeader, {
|
|
407
|
+
children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
|
|
408
|
+
onBack: onBack
|
|
409
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Content, {
|
|
410
|
+
title: "",
|
|
411
|
+
noMargin: true,
|
|
412
|
+
children: /*#__PURE__*/_jsx(TabSelector, {
|
|
413
|
+
tabs: [{
|
|
414
|
+
key: "routes",
|
|
415
|
+
label: "Routes"
|
|
416
|
+
}, {
|
|
417
|
+
key: "events",
|
|
418
|
+
label: `Events${events.length > 0 && activeTab !== "events" ? ` (${events.length})` : ""}`
|
|
419
|
+
}, {
|
|
420
|
+
key: "stack",
|
|
421
|
+
label: "Stack"
|
|
422
|
+
}],
|
|
423
|
+
activeTab: activeTab,
|
|
424
|
+
onTabChange: tab => setActiveTab(tab)
|
|
425
|
+
})
|
|
426
|
+
}), /*#__PURE__*/_jsx(ModalHeader.Actions, {
|
|
427
|
+
children: activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
|
|
428
|
+
children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
|
|
429
|
+
value: copyAllEventsData,
|
|
430
|
+
buttonStyle: styles.iconButton
|
|
431
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
432
|
+
onPress: handleToggleFilters,
|
|
433
|
+
style: [styles.iconButton, ignoredPatterns.size > 0 && styles.activeFilterButton],
|
|
434
|
+
children: /*#__PURE__*/_jsx(Filter, {
|
|
435
|
+
size: 14,
|
|
436
|
+
color: ignoredPatterns.size > 0 ? buoyColors.primary : buoyColors.textSecondary
|
|
437
|
+
})
|
|
438
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
439
|
+
onPress: handleToggleListening,
|
|
440
|
+
style: [styles.iconButton, isListening && styles.activeButton],
|
|
441
|
+
children: isListening ? /*#__PURE__*/_jsx(Pause, {
|
|
442
|
+
size: 14,
|
|
443
|
+
color: buoyColors.success
|
|
444
|
+
}) : /*#__PURE__*/_jsx(Play, {
|
|
445
|
+
size: 14,
|
|
446
|
+
color: buoyColors.success
|
|
447
|
+
})
|
|
448
|
+
}), /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
449
|
+
onPress: handleClearEvents,
|
|
450
|
+
style: styles.iconButton,
|
|
451
|
+
children: /*#__PURE__*/_jsx(Trash2, {
|
|
452
|
+
size: 14,
|
|
453
|
+
color: buoyColors.error
|
|
454
|
+
})
|
|
455
|
+
})]
|
|
456
|
+
})
|
|
457
|
+
})]
|
|
458
|
+
})
|
|
459
|
+
},
|
|
460
|
+
onModeChange: handleModeChange,
|
|
461
|
+
enablePersistence: true,
|
|
462
|
+
initialMode: "bottomSheet",
|
|
463
|
+
enableGlitchEffects: true,
|
|
464
|
+
styles: {},
|
|
465
|
+
footer: footerNode,
|
|
466
|
+
footerHeight: footerNode ? 68 : 0,
|
|
467
|
+
children: [renderContent(), /*#__PURE__*/_jsx(ProUpgradeModal, {
|
|
468
|
+
visible: showUpgradeModal,
|
|
469
|
+
onClose: () => setShowUpgradeModal(false),
|
|
470
|
+
featureName: "Route Event Management"
|
|
471
|
+
})]
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const styles = StyleSheet.create({
|
|
475
|
+
iconButton: {
|
|
476
|
+
padding: 6,
|
|
477
|
+
borderRadius: 6,
|
|
478
|
+
backgroundColor: buoyColors.input
|
|
479
|
+
},
|
|
480
|
+
activeButton: {
|
|
481
|
+
backgroundColor: buoyColors.success + "1A"
|
|
482
|
+
},
|
|
483
|
+
activeFilterButton: {
|
|
484
|
+
backgroundColor: buoyColors.primary + "1A"
|
|
485
|
+
},
|
|
486
|
+
emptyState: {
|
|
487
|
+
flex: 1,
|
|
488
|
+
justifyContent: "center",
|
|
489
|
+
alignItems: "center",
|
|
490
|
+
padding: 32
|
|
491
|
+
},
|
|
492
|
+
emptyTitle: {
|
|
493
|
+
color: buoyColors.text,
|
|
494
|
+
fontSize: 16,
|
|
495
|
+
fontWeight: "600",
|
|
496
|
+
marginTop: 16,
|
|
497
|
+
marginBottom: 8,
|
|
498
|
+
fontFamily: "monospace",
|
|
499
|
+
letterSpacing: 0.5,
|
|
500
|
+
textTransform: "uppercase"
|
|
501
|
+
},
|
|
502
|
+
emptySubtitle: {
|
|
503
|
+
color: buoyColors.textSecondary,
|
|
504
|
+
fontSize: 14,
|
|
505
|
+
textAlign: "center",
|
|
506
|
+
fontFamily: "monospace"
|
|
507
|
+
},
|
|
508
|
+
contentWrapper: {
|
|
509
|
+
flex: 1
|
|
510
|
+
},
|
|
511
|
+
statsContainer: {
|
|
512
|
+
paddingHorizontal: 16,
|
|
513
|
+
paddingVertical: 8,
|
|
514
|
+
backgroundColor: buoyColors.card,
|
|
515
|
+
borderBottomWidth: 1,
|
|
516
|
+
borderBottomColor: buoyColors.border
|
|
517
|
+
},
|
|
518
|
+
statsText: {
|
|
519
|
+
fontSize: 11,
|
|
520
|
+
color: buoyColors.textSecondary,
|
|
521
|
+
fontFamily: "monospace",
|
|
522
|
+
textAlign: "center"
|
|
523
|
+
},
|
|
524
|
+
statsWarning: {
|
|
525
|
+
color: buoyColors.warning,
|
|
526
|
+
fontWeight: "600"
|
|
527
|
+
},
|
|
528
|
+
searchContainer: {
|
|
529
|
+
paddingHorizontal: 16,
|
|
530
|
+
paddingVertical: 8,
|
|
531
|
+
backgroundColor: buoyColors.base
|
|
532
|
+
},
|
|
533
|
+
lockedEventsBanner: {
|
|
534
|
+
flexDirection: "row",
|
|
535
|
+
alignItems: "center",
|
|
536
|
+
gap: 8,
|
|
537
|
+
backgroundColor: buoyColors.warning + "15",
|
|
538
|
+
borderWidth: 1,
|
|
539
|
+
borderColor: buoyColors.warning + "30",
|
|
540
|
+
borderRadius: 8,
|
|
541
|
+
paddingVertical: 10,
|
|
542
|
+
paddingHorizontal: 14,
|
|
543
|
+
marginHorizontal: 16,
|
|
544
|
+
marginBottom: 8
|
|
545
|
+
},
|
|
546
|
+
lockedEventsBannerText: {
|
|
547
|
+
fontSize: 12,
|
|
548
|
+
fontWeight: "600",
|
|
549
|
+
color: buoyColors.warning,
|
|
550
|
+
fontFamily: "monospace"
|
|
551
|
+
},
|
|
552
|
+
lockedEventsBannerSubtext: {
|
|
553
|
+
fontSize: 11,
|
|
554
|
+
color: buoyColors.textSecondary,
|
|
555
|
+
marginLeft: "auto"
|
|
556
|
+
}
|
|
557
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RouteEventsTimeline - Chronological timeline of route navigation events
|
|
5
|
+
*
|
|
6
|
+
* Shows events in the order they happened (most recent first),
|
|
7
|
+
* providing a clear history of navigation actions.
|
|
8
|
+
*
|
|
9
|
+
* Uses two-level expansion pattern matching network/storage components.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useCallback, useState } from "react";
|
|
13
|
+
import { View, Text, ScrollView, StyleSheet } from "react-native";
|
|
14
|
+
import { Navigation, buoyColors } from "@buoy-gg/shared-ui";
|
|
15
|
+
import { RouteEventItemCompact } from "./RouteEventItemCompact";
|
|
16
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
|
+
export function RouteEventsTimeline({
|
|
18
|
+
events,
|
|
19
|
+
visitCounts,
|
|
20
|
+
onNavigate
|
|
21
|
+
}) {
|
|
22
|
+
const [expandedIndex, setExpandedIndex] = useState(null);
|
|
23
|
+
const handleItemPress = useCallback(index => {
|
|
24
|
+
setExpandedIndex(prev => prev === index ? null : index);
|
|
25
|
+
}, []);
|
|
26
|
+
if (events.length === 0) {
|
|
27
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
28
|
+
style: styles.emptyState,
|
|
29
|
+
children: [/*#__PURE__*/_jsx(Navigation, {
|
|
30
|
+
size: 48,
|
|
31
|
+
color: buoyColors.textMuted
|
|
32
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
33
|
+
style: styles.emptyTitle,
|
|
34
|
+
children: "No events yet"
|
|
35
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
36
|
+
style: styles.emptySubtitle,
|
|
37
|
+
children: "Navigation events will appear here"
|
|
38
|
+
})]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return /*#__PURE__*/_jsx(ScrollView, {
|
|
42
|
+
contentContainerStyle: styles.listContent,
|
|
43
|
+
children: events.map((event, index) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
|
|
44
|
+
event: event,
|
|
45
|
+
visitNumber: visitCounts.get(index) || 1,
|
|
46
|
+
isExpanded: expandedIndex === index,
|
|
47
|
+
onPress: () => handleItemPress(index),
|
|
48
|
+
onNavigate: onNavigate
|
|
49
|
+
}, `event-${index}-${event.timestamp}`))
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
listContent: {
|
|
54
|
+
paddingVertical: 8
|
|
55
|
+
},
|
|
56
|
+
emptyState: {
|
|
57
|
+
flex: 1,
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
padding: 32
|
|
61
|
+
},
|
|
62
|
+
emptyTitle: {
|
|
63
|
+
color: buoyColors.text,
|
|
64
|
+
fontSize: 16,
|
|
65
|
+
fontWeight: "600",
|
|
66
|
+
marginTop: 16,
|
|
67
|
+
marginBottom: 8,
|
|
68
|
+
fontFamily: "monospace",
|
|
69
|
+
letterSpacing: 0.5,
|
|
70
|
+
textTransform: "uppercase"
|
|
71
|
+
},
|
|
72
|
+
emptySubtitle: {
|
|
73
|
+
color: buoyColors.textSecondary,
|
|
74
|
+
fontSize: 14,
|
|
75
|
+
textAlign: "center",
|
|
76
|
+
fontFamily: "monospace"
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Filter, DynamicFilterView } from "@buoy-gg/shared-ui";
|
|
4
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
5
|
+
export function RouteFilterViewV2({
|
|
6
|
+
ignoredPatterns,
|
|
7
|
+
onTogglePattern,
|
|
8
|
+
onAddPattern,
|
|
9
|
+
availablePathnames = []
|
|
10
|
+
}) {
|
|
11
|
+
const filterConfig = {
|
|
12
|
+
addFilterSection: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
placeholder: "Enter pattern (e.g., /_sitemap)",
|
|
15
|
+
title: "ACTIVE FILTERS",
|
|
16
|
+
icon: Filter
|
|
17
|
+
},
|
|
18
|
+
availableItemsSection: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
title: "AVAILABLE ROUTES FROM EVENTS",
|
|
21
|
+
emptyMessage: "No routes available. Routes from navigation events will appear here.",
|
|
22
|
+
items: availablePathnames
|
|
23
|
+
},
|
|
24
|
+
howItWorksSection: {
|
|
25
|
+
enabled: true,
|
|
26
|
+
title: "HOW FILTERS WORK",
|
|
27
|
+
description: "Filtered routes will not appear in the route events list. Patterns match if the route contains the specified text.",
|
|
28
|
+
examples: ["• /_sitemap → filters /_sitemap routes", "• /api → filters /api/users, /api/posts", "• [id] → filters all routes with [id] param"],
|
|
29
|
+
icon: Filter
|
|
30
|
+
},
|
|
31
|
+
onPatternToggle: onTogglePattern,
|
|
32
|
+
onPatternAdd: onAddPattern,
|
|
33
|
+
activePatterns: ignoredPatterns
|
|
34
|
+
};
|
|
35
|
+
return /*#__PURE__*/_jsx(DynamicFilterView, {
|
|
36
|
+
...filterConfig
|
|
37
|
+
});
|
|
38
|
+
}
|