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