@buoy-gg/route-events 7.0.41 → 7.0.43

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