@buoy-gg/route-events 7.0.40 → 7.0.42

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 (104) hide show
  1. package/README.md +11 -60
  2. package/lib/commonjs/RouteObserver.js +1 -54
  3. package/lib/commonjs/RouteParser.js +1 -314
  4. package/lib/commonjs/RouteTracker.js +1 -112
  5. package/lib/commonjs/RouteTracker.web.js +1 -0
  6. package/lib/commonjs/browserRouteRegistry.js +1 -0
  7. package/lib/commonjs/browserRoutes.js +1 -0
  8. package/lib/commonjs/components/NavigationStack.js +2 -572
  9. package/lib/commonjs/components/ReactNavigationRoutes.js +1 -836
  10. package/lib/commonjs/components/ReactNavigationRoutes.web.js +1 -0
  11. package/lib/commonjs/components/RouteEventDetail.js +1 -97
  12. package/lib/commonjs/components/RouteEventExpandedContent.js +1 -204
  13. package/lib/commonjs/components/RouteEventItemCompact.js +1 -191
  14. package/lib/commonjs/components/RouteEventsModalWithTabs.js +1 -717
  15. package/lib/commonjs/components/RouteEventsTimeline.js +1 -107
  16. package/lib/commonjs/components/RouteFilterViewV2.js +1 -42
  17. package/lib/commonjs/components/RoutesSitemap.js +2 -1035
  18. package/lib/commonjs/expoRouterStore.js +1 -121
  19. package/lib/commonjs/expoRouterStore.web.js +1 -0
  20. package/lib/commonjs/hooks/useRouteEvents.js +1 -57
  21. package/lib/commonjs/index.js +1 -161
  22. package/lib/commonjs/preset.js +1 -69
  23. package/lib/commonjs/stores/navigationStackStore.js +1 -45
  24. package/lib/commonjs/stores/routeEventStore.js +1 -136
  25. package/lib/commonjs/sync/routeEventsSyncAdapter.js +1 -275
  26. package/lib/commonjs/useNavigationStack.js +1 -408
  27. package/lib/commonjs/useNavigationStack.web.js +1 -0
  28. package/lib/commonjs/useRouteObserver.js +1 -73
  29. package/lib/commonjs/useRouteObserverReactNavigation.js +1 -109
  30. package/lib/commonjs/useRouteSitemap.js +1 -255
  31. package/lib/commonjs/utils/routeTemplate.js +1 -28
  32. package/lib/commonjs/utils/safeReactNavigation.js +1 -194
  33. package/lib/commonjs/utils/safeReactNavigation.web.js +1 -0
  34. package/lib/commonjs/utils/testIDs.js +1 -63
  35. package/lib/module/RouteObserver.js +1 -49
  36. package/lib/module/RouteParser.js +1 -309
  37. package/lib/module/RouteTracker.js +1 -109
  38. package/lib/module/RouteTracker.web.js +1 -0
  39. package/lib/module/browserRouteRegistry.js +1 -0
  40. package/lib/module/browserRoutes.js +1 -0
  41. package/lib/module/components/NavigationStack.js +2 -568
  42. package/lib/module/components/ReactNavigationRoutes.js +1 -832
  43. package/lib/module/components/ReactNavigationRoutes.web.js +1 -0
  44. package/lib/module/components/RouteEventDetail.js +1 -93
  45. package/lib/module/components/RouteEventExpandedContent.js +1 -200
  46. package/lib/module/components/RouteEventItemCompact.js +1 -187
  47. package/lib/module/components/RouteEventsModalWithTabs.js +1 -714
  48. package/lib/module/components/RouteEventsTimeline.js +1 -103
  49. package/lib/module/components/RouteFilterViewV2.js +1 -38
  50. package/lib/module/components/RoutesSitemap.js +2 -1033
  51. package/lib/module/expoRouterStore.js +1 -114
  52. package/lib/module/expoRouterStore.web.js +1 -0
  53. package/lib/module/hooks/useRouteEvents.js +1 -54
  54. package/lib/module/index.js +1 -69
  55. package/lib/module/preset.js +1 -66
  56. package/lib/module/stores/navigationStackStore.js +1 -41
  57. package/lib/module/stores/routeEventStore.js +1 -128
  58. package/lib/module/sync/routeEventsSyncAdapter.js +1 -273
  59. package/lib/module/useNavigationStack.js +1 -404
  60. package/lib/module/useNavigationStack.web.js +1 -0
  61. package/lib/module/useRouteObserver.js +1 -70
  62. package/lib/module/useRouteObserverReactNavigation.js +1 -106
  63. package/lib/module/useRouteSitemap.js +1 -250
  64. package/lib/module/utils/routeTemplate.js +1 -24
  65. package/lib/module/utils/safeReactNavigation.js +1 -185
  66. package/lib/module/utils/safeReactNavigation.web.js +1 -0
  67. package/lib/module/utils/testIDs.js +1 -55
  68. package/lib/typescript/RouteTracker.web.d.ts +2 -0
  69. package/lib/typescript/browserRouteRegistry.d.ts +4 -0
  70. package/lib/typescript/browserRoutes.d.ts +2 -0
  71. package/lib/typescript/components/ReactNavigationRoutes.web.d.ts +2 -0
  72. package/lib/typescript/expoRouterStore.web.d.ts +13 -0
  73. package/lib/typescript/index.d.ts +1 -0
  74. package/lib/typescript/useNavigationStack.web.d.ts +4 -0
  75. package/lib/typescript/utils/routeTemplate.d.ts +13 -0
  76. package/lib/typescript/utils/safeReactNavigation.web.d.ts +22 -0
  77. package/lib/web/index.mjs +6 -0
  78. package/package.json +33 -13
  79. package/lib/typescript/RouteObserver.d.ts.map +0 -1
  80. package/lib/typescript/RouteParser.d.ts.map +0 -1
  81. package/lib/typescript/RouteTracker.d.ts.map +0 -1
  82. package/lib/typescript/components/NavigationStack.d.ts.map +0 -1
  83. package/lib/typescript/components/ReactNavigationRoutes.d.ts.map +0 -1
  84. package/lib/typescript/components/RouteEventDetail.d.ts.map +0 -1
  85. package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +0 -1
  86. package/lib/typescript/components/RouteEventItemCompact.d.ts.map +0 -1
  87. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +0 -1
  88. package/lib/typescript/components/RouteEventsTimeline.d.ts.map +0 -1
  89. package/lib/typescript/components/RouteFilterViewV2.d.ts.map +0 -1
  90. package/lib/typescript/components/RoutesSitemap.d.ts.map +0 -1
  91. package/lib/typescript/expoRouterStore.d.ts.map +0 -1
  92. package/lib/typescript/hooks/useRouteEvents.d.ts.map +0 -1
  93. package/lib/typescript/index.d.ts.map +0 -1
  94. package/lib/typescript/preset.d.ts.map +0 -1
  95. package/lib/typescript/stores/navigationStackStore.d.ts.map +0 -1
  96. package/lib/typescript/stores/routeEventStore.d.ts.map +0 -1
  97. package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +0 -1
  98. package/lib/typescript/useNavigationStack.d.ts.map +0 -1
  99. package/lib/typescript/useRouteObserver.d.ts.map +0 -1
  100. package/lib/typescript/useRouteObserverReactNavigation.d.ts.map +0 -1
  101. package/lib/typescript/useRouteSitemap.d.ts.map +0 -1
  102. package/lib/typescript/utils/routeTemplate.d.ts.map +0 -1
  103. package/lib/typescript/utils/safeReactNavigation.d.ts.map +0 -1
  104. package/lib/typescript/utils/testIDs.d.ts.map +0 -1
@@ -1,717 +1 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.RouteEventsModalWithTabs = RouteEventsModalWithTabs;
7
- exports.RoutesToolView = RoutesToolView;
8
- var _react = require("react");
9
- var _reactNative = require("react-native");
10
- var _safeReactNavigation = require("../utils/safeReactNavigation");
11
- var _sharedUi = require("@buoy-gg/shared-ui");
12
- var _license = require("@buoy-gg/license");
13
- var _useRouteEvents = require("../hooks/useRouteEvents");
14
- var _routeEventStore = require("../stores/routeEventStore");
15
- var _RouteEventDetail = require("./RouteEventDetail");
16
- var _RouteFilterViewV = require("./RouteFilterViewV2");
17
- var _RoutesSitemap = require("./RoutesSitemap");
18
- var _ReactNavigationRoutes = require("./ReactNavigationRoutes");
19
- var _NavigationStack = require("./NavigationStack");
20
- var _RouteEventsTimeline = require("./RouteEventsTimeline");
21
- var _jsxRuntime = require("react/jsx-runtime");
22
- // Resolved once at module load; null when React Navigation is absent
23
- // (useSafeNavigationContainerRef then yields undefined, so the dispatch
24
- // paths below never execute).
25
- const CommonActions = (0, _safeReactNavigation.getCommonActions)();
26
-
27
- // Free tier limit for route events
28
-
29
- /** Persistence recipes: the tab is stored as its bare name, the flag as "true"/"false". */
30
- const TAB_PERSISTENCE = {
31
- // Every tab the selector offers, including "stack": the save side has
32
- // always written it, so leaving it off this list meant the Stack tab
33
- // was persisted and then silently discarded on the next launch.
34
- parse: raw => raw === "routes" || raw === "events" || raw === "stack" ? raw : undefined,
35
- serialize: tab => tab
36
- };
37
- const BOOLEAN_PERSISTENCE = {
38
- parse: raw => raw === "true",
39
- serialize: value => value.toString()
40
- };
41
- const STRING_SET_PERSISTENCE = {
42
- parse: raw => new Set(JSON.parse(raw)),
43
- serialize: set => JSON.stringify(Array.from(set))
44
- };
45
-
46
- /**
47
- * Find the path from root navigation state to a screen by name.
48
- * Returns array of route names from root to the target, or null if not found.
49
- */
50
- function findScreenPath(state, screenName) {
51
- if (!state || !state.routes) return null;
52
- for (const route of state.routes) {
53
- if (route.name === screenName) {
54
- return [route.name];
55
- }
56
- if (route.state) {
57
- const nested = findScreenPath(route.state, screenName);
58
- if (nested) {
59
- return [route.name, ...nested];
60
- }
61
- }
62
- }
63
- return null;
64
- }
65
-
66
- /**
67
- * Build a nested CommonActions.navigate() from a path of screen names.
68
- * e.g. ["MainApp", "Settings", "SettingScreen"] becomes:
69
- * navigate("MainApp", { screen: "Settings", params: { screen: "SettingScreen" } })
70
- */
71
- function buildNestedNavigateAction(path) {
72
- if (path.length === 0) return null;
73
- if (path.length === 1) {
74
- return CommonActions.navigate({
75
- name: path[0]
76
- });
77
- }
78
-
79
- // Build nested params from the end
80
- let params = {
81
- screen: path[path.length - 1]
82
- };
83
- for (let i = path.length - 2; i >= 1; i--) {
84
- params = {
85
- screen: path[i],
86
- params
87
- };
88
- }
89
- return CommonActions.navigate({
90
- name: path[0],
91
- params
92
- });
93
- }
94
- function RouteEventsModalWithTabs({
95
- visible,
96
- onClose,
97
- onBack,
98
- onMinimize,
99
- enableSharedModalDimensions = false,
100
- inline = false,
101
- remote
102
- }) {
103
- const router = (0, _sharedUi.useSafeRouter)();
104
- const containerRef = (0, _safeReactNavigation.useSafeNavigationContainerRef)();
105
- const hasExpoRouter = (0, _sharedUi.isExpoRouterAvailable)();
106
- const [activeTab, setActiveTab] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.activeTab(), "events", TAB_PERSISTENCE);
107
- const [showUpgradeModal, setShowUpgradeModal] = (0, _react.useState)(false);
108
- // Serialized stack reported up by NavigationStack so the copy button can live
109
- // in the shared navbar.
110
- const [stackCopyValue, setStackCopyValue] = (0, _react.useState)("");
111
-
112
- // Check Pro status internally
113
- const isPro = (0, _license.useIsPro)();
114
- const eventLimit = (0, _sharedUi.useLimit)("routeEvents.entries");
115
-
116
- // Auto-close upgrade modal when user becomes Pro
117
- (0, _react.useEffect)(() => {
118
- if (showUpgradeModal && isPro) {
119
- setShowUpgradeModal(false);
120
- }
121
- }, [showUpgradeModal, isPro]);
122
-
123
- // NOTE: Route tracking requires <RouteTracker /> to be placed inside the navigation tree.
124
- // The useRouteObserver hook uses expo-router hooks that only work inside Stack/Tabs/Slot.
125
- // See the RouteTracker component export for easy setup.
126
-
127
- // Local state to control subscription (true = subscribed, false = unsubscribed)
128
- const [isListeningEnabled, setIsListeningEnabled] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring(), true, BOOLEAN_PERSISTENCE);
129
-
130
- // Event Listener state - using centralized store via hook
131
- // Pass enabled option to control subscription
132
- const {
133
- events,
134
- clearEvents: clearRouteEvents,
135
- isListening: isStoreListening
136
- } = (0, _useRouteEvents.useRouteEvents)({
137
- enabled: isListeningEnabled
138
- });
139
-
140
- // isListening = enabled and store is listening
141
- const isListening = isListeningEnabled && isStoreListening;
142
-
143
- // The switch above only governs THIS component's subscription; the store
144
- // also holds one of its own from boot (routeEventStore.setBootCapture), so
145
- // pausing here has to release that too or capture would never stop.
146
- (0, _react.useEffect)(() => {
147
- _routeEventStore.routeEventStore.setBootCapture(isListeningEnabled);
148
- }, [isListeningEnabled]);
149
- const [showFilters, setShowFilters] = (0, _react.useState)(false);
150
- // Selected event drives the full-screen detail view (parity with the Events
151
- // tool). null = list view. visitNumber feeds the "Visited N times" section.
152
- const [selectedEvent, setSelectedEvent] = (0, _react.useState)(null);
153
- const [selectedVisitNumber, setSelectedVisitNumber] = (0, _react.useState)(1);
154
- const [searchQuery, setSearchQuery] = (0, _react.useState)("");
155
- const [ignoredPatterns, setIgnoredPatterns] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.eventFilters(), new Set(["/_sitemap", "/api", "/__dev"]), STRING_SET_PERSISTENCE);
156
- const lastEventRef = (0, _react.useRef)(null);
157
- const handleModeChange = (0, _react.useCallback)(_mode => {
158
- // Mode changes handled by JsModal
159
- }, []);
160
- const handleNavigate = (0, _react.useCallback)(pathname => {
161
- try {
162
- // A remote host (Buoy Desktop) means the events belong to a synced
163
- // device — this process has no router, so delegate over the socket.
164
- // Same Pro gate as remote sitemap navigation: Home stays free so a
165
- // free user can always get the device back to a known screen.
166
- if (remote) {
167
- if (!remote.onNavigatePath) return;
168
- if (!isPro && pathname !== "/") {
169
- setShowUpgradeModal(true);
170
- return;
171
- }
172
- remote.onNavigatePath(pathname);
173
- return;
174
- }
175
- if (hasExpoRouter) {
176
- router.push(pathname);
177
- } else if (containerRef) {
178
- // On RN CLI, pathname is /<ScreenName> — extract the screen name
179
- const screenName = pathname.startsWith("/") ? pathname.slice(1) : pathname;
180
-
181
- // Walk the state tree to find the nesting path to this screen
182
- const rootState = containerRef.getRootState?.();
183
- if (rootState) {
184
- const path = findScreenPath(rootState, screenName);
185
- if (path) {
186
- const action = buildNestedNavigateAction(path);
187
- if (action) {
188
- containerRef.dispatch(action);
189
- return;
190
- }
191
- }
192
- }
193
-
194
- // Fallback: try direct navigation (works for top-level screens)
195
- containerRef.dispatch(CommonActions.navigate({
196
- name: screenName
197
- }));
198
- } else {
199
- // Neither Expo Router nor a navigation container — say so instead
200
- // of swallowing the tap.
201
- _reactNative.Alert.alert("Navigation Unavailable", "No Expo Router or React Navigation container was found in this app.");
202
- }
203
- } catch (error) {
204
- _reactNative.Alert.alert("Navigation Error", String(error));
205
- }
206
- }, [router, containerRef, hasExpoRouter, remote, isPro]);
207
-
208
- // Event listener is now managed by useRouteEvents hook
209
-
210
- const handleToggleListening = (0, _react.useCallback)(() => {
211
- // Toggle subscription - this actually subscribes/unsubscribes from the store
212
- setIsListeningEnabled(prev => !prev);
213
- }, []);
214
- const handleClearEvents = (0, _react.useCallback)(() => {
215
- if (events.length === 0) return;
216
-
217
- // Gate clear behind Pro
218
- if (!isPro) {
219
- setShowUpgradeModal(true);
220
- return;
221
- }
222
- clearRouteEvents();
223
- }, [events.length, isPro, clearRouteEvents]);
224
- const handleTogglePattern = (0, _react.useCallback)(pattern => {
225
- setIgnoredPatterns(prev => {
226
- const next = new Set(prev);
227
- if (next.has(pattern)) {
228
- next.delete(pattern);
229
- } else {
230
- next.add(pattern);
231
- }
232
- return next;
233
- });
234
- }, []);
235
- const handleAddPattern = (0, _react.useCallback)(pattern => {
236
- setIgnoredPatterns(prev => new Set([...prev, pattern]));
237
- }, []);
238
- const handleToggleFilters = (0, _react.useCallback)(() => {
239
- setShowFilters(!showFilters);
240
- }, [showFilters]);
241
- const handleSelectEvent = (0, _react.useCallback)((event, visitNumber) => {
242
- setSelectedEvent(event);
243
- setSelectedVisitNumber(visitNumber);
244
- }, []);
245
- const handleBackFromDetail = (0, _react.useCallback)(() => {
246
- setSelectedEvent(null);
247
- }, []);
248
- const handleTabChange = (0, _react.useCallback)(tab => {
249
- // Leaving the detail view whenever the tab changes.
250
- setSelectedEvent(null);
251
- setActiveTab(tab);
252
- }, []);
253
-
254
- // Get all unique pathnames from events
255
- const allEventPathnames = (0, _react.useMemo)(() => {
256
- const pathnames = new Set();
257
- events.forEach(event => {
258
- if (event.pathname) {
259
- pathnames.add(event.pathname);
260
- }
261
- });
262
- return Array.from(pathnames).sort();
263
- }, [events]);
264
-
265
- // Filter events based on ignored patterns and search query
266
- const filteredEvents = (0, _react.useMemo)(() => {
267
- return events.filter(event => {
268
- if (!event.pathname) return false;
269
-
270
- // Filter out pathnames that match ignored patterns
271
- const shouldIgnore = Array.from(ignoredPatterns).some(pattern => event.pathname.includes(pattern));
272
- if (shouldIgnore) return false;
273
-
274
- // Filter by search query (search in pathname and param values)
275
- if (searchQuery.trim()) {
276
- const query = searchQuery.toLowerCase();
277
- const pathnameMatch = event.pathname.toLowerCase().includes(query);
278
- const paramsMatch = Object.entries(event.params || {}).some(([key, value]) => {
279
- const valueStr = Array.isArray(value) ? value.join(" ") : value;
280
- return key.toLowerCase().includes(query) || valueStr.toLowerCase().includes(query);
281
- });
282
- return pathnameMatch || paramsMatch;
283
- }
284
- return true;
285
- });
286
- }, [events, ignoredPatterns, searchQuery]);
287
-
288
- // Pro is Infinity here, so slice() returns everything and the locked count
289
- // floors at 0 — no tier branching needed at the call site.
290
- const visibleEvents = (0, _react.useMemo)(() => filteredEvents.slice(0, eventLimit), [filteredEvents, eventLimit]);
291
- const lockedEventCount = (0, _react.useMemo)(() => Math.max(0, filteredEvents.length - eventLimit), [filteredEvents.length, eventLimit]);
292
-
293
- /**
294
- * "This was the Nth time you were here", per event.
295
- *
296
- * Keyed by the EVENT, not by its index. Indices belong to the unfiltered
297
- * list, and every consumer below reads them off the FILTERED one — so with
298
- * a search typed, or any ignore pattern active (there are three by
299
- * default), each row was handed the visit count of whatever event sat at
300
- * that position in the full history. Rows read plausibly and were simply
301
- * about a different navigation.
302
- */
303
- const visitCounts = (0, _react.useMemo)(() => {
304
- const counts = new Map();
305
- const byEvent = new Map();
306
-
307
- // Oldest to newest, so the first time you visited a route is visit 1.
308
- for (let i = events.length - 1; i >= 0; i--) {
309
- const event = events[i];
310
- const visitNumber = (counts.get(event.pathname) || 0) + 1;
311
- counts.set(event.pathname, visitNumber);
312
- byEvent.set(event, visitNumber);
313
- }
314
- return byEvent;
315
- }, [events]);
316
-
317
- // Prepare copy data - memoized so it only rebuilds when dependencies change
318
- const copyAllEventsData = (0, _react.useMemo)(() => {
319
- return {
320
- summary: {
321
- total: events.length,
322
- filtered: filteredEvents.length,
323
- listening: isListening,
324
- ignoredPatterns: Array.from(ignoredPatterns),
325
- timestamp: new Date().toISOString()
326
- },
327
- events: filteredEvents.map((event, index) => ({
328
- index,
329
- visitNumber: visitCounts.get(event) || 1,
330
- pathname: event.pathname,
331
- timestamp: event.timestamp,
332
- timestampRelative: (0, _sharedUi.formatRelativeTime)(event.timestamp),
333
- params: event.params,
334
- segments: event.segments,
335
- previousPathname: event.previousPathname,
336
- timeSincePrevious: event.timeSincePrevious
337
- }))
338
- };
339
- }, [events.length, filteredEvents, isListening, ignoredPatterns, visitCounts]);
340
- if (!visible) return null;
341
- const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : _sharedUi.devToolsStorageKeys.routeEvents.modal();
342
-
343
- // No footer needed since we're showing a timeline
344
- const footerNode = null;
345
- const renderContent = () => {
346
- // Detail view takes priority — a selected event replaces the list.
347
- if (selectedEvent) {
348
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventDetail.RouteEventDetail, {
349
- event: selectedEvent,
350
- visitNumber: selectedVisitNumber,
351
- onNavigate: handleNavigate
352
- });
353
- }
354
- if (activeTab === "routes") {
355
- // A remote host supplies the tree, so its presence — not this process's
356
- // router — decides which view can render it.
357
- return hasExpoRouter || remote?.sitemap ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_RoutesSitemap.RoutesSitemap, {
358
- style: styles.contentWrapper,
359
- injectedRoutes: remote?.sitemap?.routes,
360
- injectedSource: remote?.sitemap?.source ?? null,
361
- injectedLastUpdatedAt: remote?.sitemap?.lastUpdatedAt ?? null,
362
- onNavigateRoute: remote ? handleRemoteNavigateRoute : undefined
363
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_ReactNavigationRoutes.ReactNavigationRoutes, {
364
- style: styles.contentWrapper
365
- });
366
- }
367
- if (activeTab === "stack") {
368
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NavigationStack.NavigationStack, {
369
- style: styles.contentWrapper,
370
- injectedStack: remote ? remote.stack ?? [] : undefined,
371
- onAction: remote?.onStackAction,
372
- onCopyValueChange: setStackCopyValue
373
- });
374
- }
375
-
376
- // Events tab content
377
- if (showFilters) {
378
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteFilterViewV.RouteFilterViewV2, {
379
- ignoredPatterns: ignoredPatterns,
380
- onTogglePattern: handleTogglePattern,
381
- onAddPattern: handleAddPattern,
382
- availablePathnames: allEventPathnames
383
- });
384
- }
385
-
386
- // Show search bar if there are events (even if filtered to 0)
387
- const showSearchBar = events.length > 0;
388
- if (filteredEvents.length === 0) {
389
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
390
- style: styles.contentWrapper,
391
- testID: "buoy-routes-events",
392
- children: [showSearchBar && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
393
- style: styles.searchContainer,
394
- testID: "buoy-routes-search-field",
395
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.SearchBar, {
396
- value: searchQuery,
397
- onChange: setSearchQuery,
398
- placeholder: "Search pathname or params...",
399
- testID: "buoy-routes-search"
400
- })
401
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
402
- testID: "buoy-routes-empty",
403
- ...(0, _sharedUi.resolveListEmptyState)({
404
- subject: "route events",
405
- search: searchQuery,
406
- total: events.length,
407
- capturing: isListening,
408
- waiting: {
409
- otto: "walking",
410
- title: "No route events yet",
411
- message: "Navigation events will appear here"
412
- },
413
- paused: {
414
- title: "Event listener is paused",
415
- message: "Press play to start monitoring"
416
- }
417
- })
418
- })]
419
- });
420
- }
421
-
422
- // Show chronological timeline of events
423
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
424
- style: styles.contentWrapper,
425
- testID: "buoy-routes-events",
426
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
427
- style: styles.searchContainer,
428
- testID: "buoy-routes-search-field",
429
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.SearchBar, {
430
- value: searchQuery,
431
- onChange: setSearchQuery,
432
- placeholder: "Search pathname or params...",
433
- testID: "buoy-routes-search"
434
- })
435
- }), lockedEventCount > 0 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
436
- style: styles.lockedEventsBanner,
437
- testID: "buoy-routes-locked-banner",
438
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
439
- size: 14,
440
- color: _sharedUi.night.color.warning
441
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
442
- style: styles.lockedEventsBannerText,
443
- testID: "buoy-routes-locked-banner-text",
444
- children: [lockedEventCount, " older ", lockedEventCount === 1 ? 'event' : 'events', " locked"]
445
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
446
- style: styles.lockedEventsBannerSubtext,
447
- children: "Upgrade to Pro for full history"
448
- })]
449
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventsTimeline.RouteEventsTimeline, {
450
- events: visibleEvents,
451
- visitCounts: visitCounts,
452
- onNavigate: handleNavigate,
453
- onSelectEvent: handleSelectEvent
454
- })]
455
- });
456
- };
457
- const headerContent = selectedEvent ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
458
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
459
- onBack: handleBackFromDetail
460
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
461
- title: selectedEvent.pathname,
462
- testID: "buoy-routes-detail-title"
463
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
464
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
465
- testID: "buoy-routes-detail-copy",
466
- value: {
467
- pathname: selectedEvent.pathname,
468
- previousPathname: selectedEvent.previousPathname,
469
- params: selectedEvent.params,
470
- segments: selectedEvent.segments,
471
- timestamp: selectedEvent.timestamp,
472
- timestampISO: new Date(selectedEvent.timestamp).toISOString(),
473
- timeSincePrevious: selectedEvent.timeSincePrevious,
474
- visitNumber: selectedVisitNumber
475
- },
476
- buttonStyle: styles.iconButton
477
- })
478
- })]
479
- }) : showFilters ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
480
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
481
- onBack: () => setShowFilters(false)
482
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
483
- title: "Filters",
484
- testID: "buoy-routes-filters-title"
485
- })]
486
- }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
487
- children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
488
- onBack: onBack
489
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
490
- title: "",
491
- noMargin: true,
492
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
493
- tabs: [{
494
- key: "routes",
495
- label: hasExpoRouter ? "Routes" : "Screens"
496
- }, {
497
- key: "events",
498
- label: `Events${events.length > 0 && activeTab !== "events" ? ` (${events.length})` : ""}`
499
- }, {
500
- key: "stack",
501
- label: "Stack"
502
- }],
503
- activeTab: activeTab,
504
- onTabChange: tab => handleTabChange(tab),
505
- testIDPrefix: "buoy-routes-tab"
506
- })
507
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
508
- children: [activeTab === "stack" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
509
- value: stackCopyValue,
510
- buttonStyle: styles.iconButton,
511
- testID: "buoy-routes-stack-copy"
512
- }), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
513
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
514
- value: copyAllEventsData,
515
- buttonStyle: styles.iconButton,
516
- testID: "buoy-routes-copy"
517
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
518
- onPress: handleToggleFilters,
519
- style: [styles.iconButton, ignoredPatterns.size > 0 && styles.activeFilterButton],
520
- testID: "buoy-routes-filters",
521
- accessibilityRole: "button",
522
- accessibilityLabel: ignoredPatterns.size > 0 ? `Route filters, ${ignoredPatterns.size} active` : "Route filters",
523
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
524
- size: 14,
525
- color: ignoredPatterns.size > 0 ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
526
- })
527
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
528
- isEnabled: isListening,
529
- onToggle: handleToggleListening,
530
- accessibilityLabel: "Toggle route event monitoring",
531
- testID: "buoy-routes-power"
532
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
533
- onPress: handleClearEvents,
534
- style: styles.iconButton,
535
- testID: "buoy-routes-clear",
536
- accessibilityRole: "button",
537
- accessibilityLabel: "Clear route events",
538
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
539
- size: 14,
540
- color: _sharedUi.night.color.danger
541
- })
542
- })]
543
- })]
544
- })]
545
- });
546
-
547
- /**
548
- * Pro gate for remote sitemap navigation. The Home shortcut stays free so a
549
- * free user can always get the device back to a known screen; layouts and
550
- * groups are not navigable at all.
551
- */
552
- const handleRemoteNavigateRoute = route => {
553
- if (route.type === "layout" || route.type === "group") return;
554
- const isHomeRoute = route.type === "index" || route.path === "/";
555
- if (!isPro && !isHomeRoute) {
556
- setShowUpgradeModal(true);
557
- return;
558
- }
559
- remote?.onNavigateRoute?.(route);
560
- };
561
- const upgradeModal = /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
562
- visible: showUpgradeModal,
563
- onClose: () => setShowUpgradeModal(false),
564
- featureName: "Route Event Management"
565
- });
566
-
567
- // The ONLY difference between the phone and the desktop dashboard is which
568
- // shell the header, body and footer go in. Everything above this is shared.
569
- if (inline) {
570
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
571
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
572
- style: styles.inlineRoot,
573
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
574
- style: styles.inlineHeader,
575
- ..._sharedUi.windowDragProps,
576
- children: headerContent
577
- }), renderContent(), footerNode]
578
- }), upgradeModal]
579
- });
580
- }
581
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TickProvider, {
582
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.JsModal, {
583
- visible: visible,
584
- onClose: onClose,
585
- onMinimize: onMinimize,
586
- persistenceKey: persistenceKey,
587
- header: {
588
- showToggleButton: true,
589
- customContent: headerContent
590
- },
591
- onModeChange: handleModeChange,
592
- enablePersistence: true,
593
- initialMode: "bottomSheet",
594
- enableGlitchEffects: true
595
- // Every screen owns its scrolling (timeline FlatList, sitemap/stack/
596
- // detail/filter ScrollViews) — the default wrapper would nest them.
597
- ,
598
- disableScrollWrapper: true,
599
- styles: {},
600
- footer: footerNode,
601
- footerHeight: footerNode ? 68 : 0,
602
- children: [renderContent(), upgradeModal]
603
- })
604
- });
605
- }
606
-
607
- /**
608
- * The same routes tool as `RouteEventsModalWithTabs`, rendered INLINE for hosts
609
- * that place tools in a panel rather than a modal. Exists so there is exactly
610
- * one implementation — the desktop dashboard used to ship its own copy of this
611
- * component's header and tab router.
612
- */
613
- function RoutesToolView(props = {}) {
614
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteEventsModalWithTabs, {
615
- ...props,
616
- inline: true,
617
- visible: true,
618
- onClose: () => {}
619
- });
620
- }
621
- const styles = _reactNative.StyleSheet.create({
622
- inlineRoot: {
623
- flex: 1,
624
- minHeight: 0,
625
- // Transparent so the desktop workbench's hoisted sky (one field spanning
626
- // sidebar + panel) shows through — the shell paints its base beneath it.
627
- backgroundColor: "transparent"
628
- },
629
- inlineHeader: {
630
- paddingHorizontal: 12,
631
- paddingVertical: 8,
632
- borderBottomWidth: 1,
633
- borderBottomColor: _sharedUi.buoyColors.border,
634
- // Clear, like Bench — the host's hoisted sky shows through the header.
635
- backgroundColor: "transparent"
636
- },
637
- iconButton: {
638
- padding: 6,
639
- borderRadius: 6,
640
- backgroundColor: _sharedUi.night.color.buttonSurface
641
- },
642
- activeFilterButton: {
643
- backgroundColor: _sharedUi.night.color.accentSoft
644
- },
645
- emptyState: {
646
- flex: 1,
647
- justifyContent: "center",
648
- alignItems: "center",
649
- padding: 32
650
- },
651
- emptyTitle: {
652
- color: _sharedUi.night.color.text,
653
- fontSize: 16,
654
- fontWeight: "600",
655
- marginTop: 16,
656
- marginBottom: 8,
657
- fontFamily: _sharedUi.monoFont,
658
- letterSpacing: 0.5,
659
- textTransform: "uppercase"
660
- },
661
- emptySubtitle: {
662
- color: _sharedUi.night.color.textSecondary,
663
- fontSize: 14,
664
- textAlign: "center",
665
- fontFamily: _sharedUi.monoFont
666
- },
667
- contentWrapper: {
668
- flex: 1
669
- },
670
- statsContainer: {
671
- paddingHorizontal: 16,
672
- paddingVertical: 8,
673
- backgroundColor: _sharedUi.night.color.surface,
674
- borderBottomWidth: 1,
675
- borderBottomColor: _sharedUi.night.color.border
676
- },
677
- statsText: {
678
- fontSize: 11,
679
- color: _sharedUi.night.color.textSecondary,
680
- fontFamily: _sharedUi.monoFont,
681
- textAlign: "center"
682
- },
683
- statsWarning: {
684
- color: _sharedUi.night.color.warning,
685
- fontWeight: "600"
686
- },
687
- searchContainer: {
688
- paddingHorizontal: 16,
689
- paddingVertical: 8,
690
- // Transparent so the night sheet's star field shows through.
691
- backgroundColor: "transparent"
692
- },
693
- lockedEventsBanner: {
694
- flexDirection: "row",
695
- alignItems: "center",
696
- gap: 8,
697
- backgroundColor: _sharedUi.night.color.warningSoft,
698
- borderWidth: 1,
699
- borderColor: _sharedUi.night.color.warning + "30",
700
- borderRadius: 8,
701
- paddingVertical: 10,
702
- paddingHorizontal: 14,
703
- marginHorizontal: 16,
704
- marginBottom: 8
705
- },
706
- lockedEventsBannerText: {
707
- fontSize: 12,
708
- fontWeight: "600",
709
- color: _sharedUi.night.color.warning,
710
- fontFamily: _sharedUi.monoFont
711
- },
712
- lockedEventsBannerSubtext: {
713
- fontSize: 11,
714
- color: _sharedUi.night.color.textSecondary,
715
- marginLeft: "auto"
716
- }
717
- });
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.RouteEventsModalWithTabs=RouteEventsModalWithTabs;exports.RoutesToolView=RoutesToolView;var _react=require("react");var _reactNative=require("react-native");var _safeReactNavigation=require("../utils/safeReactNavigation");var _sharedUi=require("@buoy-gg/shared-ui");var _license=require("@buoy-gg/license");var _useRouteEvents=require("../hooks/useRouteEvents");var _routeEventStore=require("../stores/routeEventStore");var _RouteEventDetail=require("./RouteEventDetail");var _RouteFilterViewV=require("./RouteFilterViewV2");var _RoutesSitemap=require("./RoutesSitemap");var _ReactNavigationRoutes=require("./ReactNavigationRoutes");var _NavigationStack=require("./NavigationStack");var _RouteEventsTimeline=require("./RouteEventsTimeline");var _routeTemplate=require("../utils/routeTemplate");var _jsxRuntime=require("react/jsx-runtime");const CommonActions=(0,_safeReactNavigation.getCommonActions)();const TAB_PERSISTENCE={parse:t=>t==="routes"||t==="events"||t==="stack"?t:void 0,serialize:t=>t};const BOOLEAN_PERSISTENCE={parse:t=>t==="true",serialize:t=>t.toString()};const STRING_SET_PERSISTENCE={parse:t=>new Set(JSON.parse(t)),serialize:t=>JSON.stringify(Array.from(t))};function findScreenPath(t,u){if(!t||!t.routes)return null;for(const a of t.routes){if(a.name===u){return[a.name]}if(a.state){const d=findScreenPath(a.state,u);if(d){return[a.name,...d]}}}return null}function buildNestedNavigateAction(t,u){if(t.length===0)return null;const a=u&&Object.keys(u).length>0?u:void 0;if(t.length===1){return CommonActions.navigate(a?{name:t[0],params:a}:{name:t[0]})}let d=a?{screen:t[t.length-1],params:a}:{screen:t[t.length-1]};for(let b=t.length-2;b>=1;b--){d={screen:t[b],params:d}}return CommonActions.navigate({name:t[0],params:d})}function RouteEventsModalWithTabs({visible:t,onClose:u,onBack:a,onMinimize:d,enableSharedModalDimensions:b=false,inline:q=false,remote:r}){const I=(0,_sharedUi.useSafeRouter)();const y=(0,_safeReactNavigation.useSafeNavigationContainerRef)();const x=(0,_sharedUi.isExpoRouterAvailable)();const[m,O]=(0,_sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.activeTab(),"events",TAB_PERSISTENCE);const[N,S]=(0,_react.useState)(false);const[U,K]=(0,_react.useState)("");const h=(0,_license.useIsPro)();const E=(0,_sharedUi.useLimit)("routeEvents.entries");(0,_react.useEffect)(()=>{if(N&&h){S(false)}},[N,h]);const[C,J]=(0,_sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring(),true,BOOLEAN_PERSISTENCE);const{events:s,clearEvents:D,isListening:$}=(0,_useRouteEvents.useRouteEvents)({enabled:C});const R=C&&$;(0,_react.useEffect)(()=>{_routeEventStore.routeEventStore.setBootCapture(C)},[C]);const[j,A]=(0,_react.useState)(false);const[i,w]=(0,_react.useState)(null);const[_,G]=(0,_react.useState)(1);const[v,V]=(0,_react.useState)("");const[l,H]=(0,_sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.eventFilters(),new Set(["/_sitemap","/api","/__dev"]),STRING_SET_PERSISTENCE);const ue=(0,_react.useRef)(null);const Q=(0,_react.useCallback)(e=>{},[]);const z=(0,_react.useCallback)(e=>{try{if(r){if(!r.onNavigatePath)return;if(!h&&e!=="/"){S(true);return}r.onNavigatePath(e);return}if(x){I.push(e)}else if(y){const{pathname:n,params:o}=(0,_routeTemplate.splitRouteHref)(e);const c=n.startsWith("/")?n.slice(1):n;const p=Object.keys(o).length>0;const k=y.getRootState?.();if(k){const f=findScreenPath(k,c);if(f){const T=buildNestedNavigateAction(f,o);if(T){y.dispatch(T);return}}}y.dispatch(CommonActions.navigate(p?{name:c,params:o}:{name:c}))}else{_reactNative.Alert.alert("Navigation Unavailable","No Expo Router or React Navigation container was found in this app.")}}catch(n){_reactNative.Alert.alert("Navigation Error",String(n))}},[I,y,x,r,h]);const X=(0,_react.useCallback)(()=>{J(e=>!e)},[]);const Y=(0,_react.useCallback)(()=>{if(s.length===0)return;if(!h){S(true);return}D()},[s.length,h,D]);const Z=(0,_react.useCallback)(e=>{H(n=>{const o=new Set(n);if(o.has(e)){o.delete(e)}else{o.add(e)}return o})},[]);const ee=(0,_react.useCallback)(e=>{H(n=>new Set([...n,e]))},[]);const te=(0,_react.useCallback)(()=>{A(!j)},[j]);const ne=(0,_react.useCallback)((e,n)=>{w(e);G(n)},[]);const oe=(0,_react.useCallback)(()=>{w(null)},[]);const se=(0,_react.useCallback)(e=>{w(null);O(e)},[]);const ae=(0,_react.useMemo)(()=>{const e=new Set;s.forEach(n=>{if(n.pathname){e.add(n.pathname)}});return Array.from(e).sort()},[s]);const g=(0,_react.useMemo)(()=>{return s.filter(e=>{if(!e.pathname)return false;const n=Array.from(l).some(o=>e.pathname.includes(o));if(n)return false;if(v.trim()){const o=v.toLowerCase();const c=e.pathname.toLowerCase().includes(o);const p=Object.entries(e.params||{}).some(([k,f])=>{const T=Array.isArray(f)?f.join(" "):f;return k.toLowerCase().includes(o)||T.toLowerCase().includes(o)});return c||p}return true})},[s,l,v]);const re=(0,_react.useMemo)(()=>g.slice(0,E),[g,E]);const P=(0,_react.useMemo)(()=>Math.max(0,g.length-E),[g.length,E]);const M=(0,_react.useMemo)(()=>{const e=new Map;const n=new Map;for(let o=s.length-1;o>=0;o--){const c=s[o];const p=(e.get(c.pathname)||0)+1;e.set(c.pathname,p);n.set(c,p)}return n},[s]);const ie=(0,_react.useMemo)(()=>{return{summary:{total:s.length,filtered:g.length,listening:R,ignoredPatterns:Array.from(l),timestamp:new Date().toISOString()},events:g.map((e,n)=>({index:n,visitNumber:M.get(e)||1,pathname:e.pathname,timestamp:e.timestamp,timestampRelative:(0,_sharedUi.formatRelativeTime)(e.timestamp),params:e.params,segments:e.segments,previousPathname:e.previousPathname,timeSincePrevious:e.timeSincePrevious}))}},[s.length,g,R,l,M]);if(!t)return null;const le=b?_sharedUi.devToolsStorageKeys.modal.root():_sharedUi.devToolsStorageKeys.routeEvents.modal();const B=null;const F=()=>{if(i){return(0,_jsxRuntime.jsx)(_RouteEventDetail.RouteEventDetail,{event:i,visitNumber:_,onNavigate:z})}if(m==="routes"){return x||r?.sitemap?(0,_jsxRuntime.jsx)(_RoutesSitemap.RoutesSitemap,{style:styles.contentWrapper,injectedRoutes:r?.sitemap?.routes,injectedSource:r?.sitemap?.source??null,injectedLastUpdatedAt:r?.sitemap?.lastUpdatedAt??null,onNavigateRoute:r?ce:void 0}):(0,_jsxRuntime.jsx)(_ReactNavigationRoutes.ReactNavigationRoutes,{style:styles.contentWrapper})}if(m==="stack"){return(0,_jsxRuntime.jsx)(_NavigationStack.NavigationStack,{style:styles.contentWrapper,injectedStack:r?r.stack??[]:void 0,onAction:r?.onStackAction,onCopyValueChange:K})}if(j){return(0,_jsxRuntime.jsx)(_RouteFilterViewV.RouteFilterViewV2,{ignoredPatterns:l,onTogglePattern:Z,onAddPattern:ee,availablePathnames:ae})}const e=s.length>0;if(g.length===0){return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.contentWrapper,testID:"buoy-routes-events",children:[e&&(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.searchContainer,testID:"buoy-routes-search-field",children:(0,_jsxRuntime.jsx)(_sharedUi.SearchBar,{value:v,onChange:V,placeholder:"Search pathname or params...",testID:"buoy-routes-search"})}),(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{testID:"buoy-routes-empty",...(0,_sharedUi.resolveListEmptyState)({subject:"route events",search:v,total:s.length,capturing:R,waiting:{otto:"walking",title:"No route events yet",message:"Navigation events will appear here"},paused:{title:"Event listener is paused",message:"Press play to start monitoring"}})})]})}return(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.contentWrapper,testID:"buoy-routes-events",children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.searchContainer,testID:"buoy-routes-search-field",children:(0,_jsxRuntime.jsx)(_sharedUi.SearchBar,{value:v,onChange:V,placeholder:"Search pathname or params...",testID:"buoy-routes-search"})}),P>0&&(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.lockedEventsBanner,testID:"buoy-routes-locked-banner",children:[(0,_jsxRuntime.jsx)(_sharedUi.Lock,{size:14,color:_sharedUi.night.color.warning}),(0,_jsxRuntime.jsxs)(_reactNative.Text,{style:styles.lockedEventsBannerText,testID:"buoy-routes-locked-banner-text",children:[P," older ",P===1?"event":"events"," locked"]}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.lockedEventsBannerSubtext,children:"Upgrade to Pro for full history"})]}),(0,_jsxRuntime.jsx)(_RouteEventsTimeline.RouteEventsTimeline,{events:re,visitCounts:M,onNavigate:z,onSelectEvent:ne})]})};const L=i?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:oe}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:i.pathname,testID:"buoy-routes-detail-title"}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions,{children:(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{testID:"buoy-routes-detail-copy",value:{pathname:i.pathname,previousPathname:i.previousPathname,params:i.params,segments:i.segments,timestamp:i.timestamp,timestampISO:new Date(i.timestamp).toISOString(),timeSincePrevious:i.timeSincePrevious,visitNumber:_},buttonStyle:styles.iconButton})})]}):j?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:()=>A(false)}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"Filters",testID:"buoy-routes-filters-title"})]}):(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[a&&(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:a}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"",noMargin:true,children:(0,_jsxRuntime.jsx)(_sharedUi.TabSelector,{tabs:[{key:"routes",label:x?"Routes":"Screens"},{key:"events",label:`Events${s.length>0&&m!=="events"?` (${s.length})`:""}`},{key:"stack",label:"Stack"}],activeTab:m,onTabChange:e=>se(e),testIDPrefix:"buoy-routes-tab"})}),(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions,{children:[m==="stack"&&(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:U,buttonStyle:styles.iconButton,testID:"buoy-routes-stack-copy"}),m==="events"&&(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:ie,buttonStyle:styles.iconButton,testID:"buoy-routes-copy"}),(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:te,style:[styles.iconButton,l.size>0&&styles.activeFilterButton],testID:"buoy-routes-filters",accessibilityRole:"button",accessibilityLabel:l.size>0?`Route filters, ${l.size} active`:"Route filters",children:(0,_jsxRuntime.jsx)(_sharedUi.Filter,{size:14,color:l.size>0?_sharedUi.night.color.accent:_sharedUi.night.color.textSecondary})}),(0,_jsxRuntime.jsx)(_sharedUi.PowerToggleButton,{isEnabled:R,onToggle:X,accessibilityLabel:"Toggle route event monitoring",testID:"buoy-routes-power"}),(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:Y,style:styles.iconButton,testID:"buoy-routes-clear",accessibilityRole:"button",accessibilityLabel:"Clear route events",children:(0,_jsxRuntime.jsx)(_sharedUi.Trash2,{size:14,color:_sharedUi.night.color.danger})})]})]})]});const ce=e=>{if(e.type==="layout"||e.type==="group")return;const n=e.type==="index"||e.path==="/";if(!h&&!n){S(true);return}r?.onNavigateRoute?.(e)};const W=(0,_jsxRuntime.jsx)(_sharedUi.ProUpgradeModal,{visible:N,onClose:()=>S(false),featureName:"Route Event Management"});if(q){return(0,_jsxRuntime.jsxs)(_sharedUi.TickProvider,{children:[(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.inlineRoot,children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.inlineHeader,..._sharedUi.windowDragProps,children:L}),F(),B]}),W]})}return(0,_jsxRuntime.jsx)(_sharedUi.TickProvider,{children:(0,_jsxRuntime.jsxs)(_sharedUi.JsModal,{visible:t,onClose:u,onMinimize:d,persistenceKey:le,header:{showToggleButton:true,customContent:L},onModeChange:Q,enablePersistence:true,initialMode:"bottomSheet",enableGlitchEffects:true,disableScrollWrapper:true,styles:{},footer:B,footerHeight:B?68:0,children:[F(),W]})})}function RoutesToolView(t={}){return(0,_jsxRuntime.jsx)(RouteEventsModalWithTabs,{...t,inline:true,visible:true,onClose:()=>{}})}const styles=_reactNative.StyleSheet.create({inlineRoot:{flex:1,minHeight:0,backgroundColor:"transparent"},inlineHeader:{paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:_sharedUi.buoyColors.border,backgroundColor:"transparent"},iconButton:{padding:6,borderRadius:6,backgroundColor:_sharedUi.night.color.buttonSurface},activeFilterButton:{backgroundColor:_sharedUi.night.color.accentSoft},emptyState:{flex:1,justifyContent:"center",alignItems:"center",padding:32},emptyTitle:{color:_sharedUi.night.color.text,fontSize:16,fontWeight:"600",marginTop:16,marginBottom:8,fontFamily:_sharedUi.monoFont,letterSpacing:.5,textTransform:"uppercase"},emptySubtitle:{color:_sharedUi.night.color.textSecondary,fontSize:14,textAlign:"center",fontFamily:_sharedUi.monoFont},contentWrapper:{flex:1},statsContainer:{paddingHorizontal:16,paddingVertical:8,backgroundColor:_sharedUi.night.color.surface,borderBottomWidth:1,borderBottomColor:_sharedUi.night.color.border},statsText:{fontSize:11,color:_sharedUi.night.color.textSecondary,fontFamily:_sharedUi.monoFont,textAlign:"center"},statsWarning:{color:_sharedUi.night.color.warning,fontWeight:"600"},searchContainer:{paddingHorizontal:16,paddingVertical:8,backgroundColor:"transparent"},lockedEventsBanner:{flexDirection:"row",alignItems:"center",gap:8,backgroundColor:_sharedUi.night.color.warningSoft,borderWidth:1,borderColor:_sharedUi.night.color.warning+"30",borderRadius:8,paddingVertical:10,paddingHorizontal:14,marginHorizontal:16,marginBottom:8},lockedEventsBannerText:{fontSize:12,fontWeight:"600",color:_sharedUi.night.color.warning,fontFamily:_sharedUi.monoFont},lockedEventsBannerSubtext:{fontSize:11,color:_sharedUi.night.color.textSecondary,marginLeft:"auto"}});