@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.
Files changed (79) hide show
  1. package/README.md +654 -0
  2. package/lib/commonjs/RouteObserver.js +54 -0
  3. package/lib/commonjs/RouteParser.js +310 -0
  4. package/lib/commonjs/RouteTracker.js +39 -0
  5. package/lib/commonjs/components/NavigationStack.js +584 -0
  6. package/lib/commonjs/components/RouteEventDetailContent.js +492 -0
  7. package/lib/commonjs/components/RouteEventExpandedContent.js +187 -0
  8. package/lib/commonjs/components/RouteEventItemCompact.js +175 -0
  9. package/lib/commonjs/components/RouteEventsModalWithTabs.js +560 -0
  10. package/lib/commonjs/components/RouteEventsTimeline.js +82 -0
  11. package/lib/commonjs/components/RouteFilterViewV2.js +42 -0
  12. package/lib/commonjs/components/RoutesSitemap.js +948 -0
  13. package/lib/commonjs/expoRouterStore.js +104 -0
  14. package/lib/commonjs/index.js +99 -0
  15. package/lib/commonjs/package.json +1 -0
  16. package/lib/commonjs/preset.js +83 -0
  17. package/lib/commonjs/useNavigationStack.js +241 -0
  18. package/lib/commonjs/useRouteObserver.js +73 -0
  19. package/lib/commonjs/useRouteSitemap.js +234 -0
  20. package/lib/commonjs/utils/safeExpoRouter.js +129 -0
  21. package/lib/commonjs/utils/safeReactNavigation.js +104 -0
  22. package/lib/module/RouteObserver.js +49 -0
  23. package/lib/module/RouteParser.js +305 -0
  24. package/lib/module/RouteTracker.js +35 -0
  25. package/lib/module/components/NavigationStack.js +580 -0
  26. package/lib/module/components/RouteEventDetailContent.js +487 -0
  27. package/lib/module/components/RouteEventExpandedContent.js +183 -0
  28. package/lib/module/components/RouteEventItemCompact.js +171 -0
  29. package/lib/module/components/RouteEventsModalWithTabs.js +557 -0
  30. package/lib/module/components/RouteEventsTimeline.js +78 -0
  31. package/lib/module/components/RouteFilterViewV2.js +38 -0
  32. package/lib/module/components/RoutesSitemap.js +944 -0
  33. package/lib/module/expoRouterStore.js +98 -0
  34. package/lib/module/index.js +23 -0
  35. package/lib/module/preset.js +79 -0
  36. package/lib/module/useNavigationStack.js +238 -0
  37. package/lib/module/useRouteObserver.js +70 -0
  38. package/lib/module/useRouteSitemap.js +229 -0
  39. package/lib/module/utils/safeExpoRouter.js +120 -0
  40. package/lib/module/utils/safeReactNavigation.js +98 -0
  41. package/lib/typescript/RouteObserver.d.ts +37 -0
  42. package/lib/typescript/RouteObserver.d.ts.map +1 -0
  43. package/lib/typescript/RouteParser.d.ts +129 -0
  44. package/lib/typescript/RouteParser.d.ts.map +1 -0
  45. package/lib/typescript/RouteTracker.d.ts +29 -0
  46. package/lib/typescript/RouteTracker.d.ts.map +1 -0
  47. package/lib/typescript/components/NavigationStack.d.ts +11 -0
  48. package/lib/typescript/components/NavigationStack.d.ts.map +1 -0
  49. package/lib/typescript/components/RouteEventDetailContent.d.ts +21 -0
  50. package/lib/typescript/components/RouteEventDetailContent.d.ts.map +1 -0
  51. package/lib/typescript/components/RouteEventExpandedContent.d.ts +16 -0
  52. package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +1 -0
  53. package/lib/typescript/components/RouteEventItemCompact.d.ts +15 -0
  54. package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -0
  55. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts +15 -0
  56. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -0
  57. package/lib/typescript/components/RouteEventsTimeline.d.ts +17 -0
  58. package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -0
  59. package/lib/typescript/components/RouteFilterViewV2.d.ts +9 -0
  60. package/lib/typescript/components/RouteFilterViewV2.d.ts.map +1 -0
  61. package/lib/typescript/components/RoutesSitemap.d.ts +15 -0
  62. package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -0
  63. package/lib/typescript/expoRouterStore.d.ts +28 -0
  64. package/lib/typescript/expoRouterStore.d.ts.map +1 -0
  65. package/lib/typescript/index.d.ts +18 -0
  66. package/lib/typescript/index.d.ts.map +1 -0
  67. package/lib/typescript/preset.d.ts +76 -0
  68. package/lib/typescript/preset.d.ts.map +1 -0
  69. package/lib/typescript/useNavigationStack.d.ts +48 -0
  70. package/lib/typescript/useNavigationStack.d.ts.map +1 -0
  71. package/lib/typescript/useRouteObserver.d.ts +27 -0
  72. package/lib/typescript/useRouteObserver.d.ts.map +1 -0
  73. package/lib/typescript/useRouteSitemap.d.ts +102 -0
  74. package/lib/typescript/useRouteSitemap.d.ts.map +1 -0
  75. package/lib/typescript/utils/safeExpoRouter.d.ts +13 -0
  76. package/lib/typescript/utils/safeExpoRouter.d.ts.map +1 -0
  77. package/lib/typescript/utils/safeReactNavigation.d.ts +10 -0
  78. package/lib/typescript/utils/safeReactNavigation.d.ts.map +1 -0
  79. 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
+ }