@buoy-gg/route-events 7.0.33 → 7.0.34

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.
@@ -25,6 +25,23 @@ const CommonActions = (0, _safeReactNavigation.getCommonActions)();
25
25
 
26
26
  // Free tier limit for route events
27
27
 
28
+ /** Persistence recipes: the tab is stored as its bare name, the flag as "true"/"false". */
29
+ const TAB_PERSISTENCE = {
30
+ // Every tab the selector offers, including "stack": the save side has
31
+ // always written it, so leaving it off this list meant the Stack tab
32
+ // was persisted and then silently discarded on the next launch.
33
+ parse: raw => raw === "routes" || raw === "events" || raw === "stack" ? raw : undefined,
34
+ serialize: tab => tab
35
+ };
36
+ const BOOLEAN_PERSISTENCE = {
37
+ parse: raw => raw === "true",
38
+ serialize: value => value.toString()
39
+ };
40
+ const STRING_SET_PERSISTENCE = {
41
+ parse: raw => new Set(JSON.parse(raw)),
42
+ serialize: set => JSON.stringify(Array.from(set))
43
+ };
44
+
28
45
  /**
29
46
  * Find the path from root navigation state to a screen by name.
30
47
  * Returns array of route names from root to the target, or null if not found.
@@ -85,7 +102,7 @@ function RouteEventsModalWithTabs({
85
102
  const router = (0, _sharedUi.useSafeRouter)();
86
103
  const containerRef = (0, _safeReactNavigation.useSafeNavigationContainerRef)();
87
104
  const hasExpoRouter = (0, _sharedUi.isExpoRouterAvailable)();
88
- const [activeTab, setActiveTab] = (0, _react.useState)("events");
105
+ const [activeTab, setActiveTab] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.activeTab(), "events", TAB_PERSISTENCE);
89
106
  const [showUpgradeModal, setShowUpgradeModal] = (0, _react.useState)(false);
90
107
  // Serialized stack reported up by NavigationStack so the copy button can live
91
108
  // in the shared navbar.
@@ -107,7 +124,7 @@ function RouteEventsModalWithTabs({
107
124
  // See the RouteTracker component export for easy setup.
108
125
 
109
126
  // Local state to control subscription (true = subscribed, false = unsubscribed)
110
- const [isListeningEnabled, setIsListeningEnabled] = (0, _react.useState)(true);
127
+ const [isListeningEnabled, setIsListeningEnabled] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring(), true, BOOLEAN_PERSISTENCE);
111
128
 
112
129
  // Event Listener state - using centralized store via hook
113
130
  // Pass enabled option to control subscription
@@ -127,11 +144,8 @@ function RouteEventsModalWithTabs({
127
144
  const [selectedEvent, setSelectedEvent] = (0, _react.useState)(null);
128
145
  const [selectedVisitNumber, setSelectedVisitNumber] = (0, _react.useState)(1);
129
146
  const [searchQuery, setSearchQuery] = (0, _react.useState)("");
130
- const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set(["/_sitemap", "/api", "/__dev"]));
147
+ const [ignoredPatterns, setIgnoredPatterns] = (0, _sharedUi.usePersistedState)(_sharedUi.devToolsStorageKeys.routeEvents.eventFilters(), new Set(["/_sitemap", "/api", "/__dev"]), STRING_SET_PERSISTENCE);
131
148
  const lastEventRef = (0, _react.useRef)(null);
132
- const hasLoadedFilters = (0, _react.useRef)(false);
133
- const hasLoadedTabState = (0, _react.useRef)(false);
134
- const hasLoadedMonitoringState = (0, _react.useRef)(false);
135
149
  const handleModeChange = (0, _react.useCallback)(_mode => {
136
150
  // Mode changes handled by JsModal
137
151
  }, []);
@@ -183,103 +197,6 @@ function RouteEventsModalWithTabs({
183
197
  }
184
198
  }, [router, containerRef, hasExpoRouter, remote, isPro]);
185
199
 
186
- // Load persisted tab state on mount
187
- (0, _react.useEffect)(() => {
188
- if (!visible || hasLoadedTabState.current) return;
189
- const loadTabState = async () => {
190
- try {
191
- const storedTab = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.routeEvents.activeTab());
192
- // Every tab the selector offers, including "stack": the save side has
193
- // always written it, so leaving it off this list meant the Stack tab
194
- // was persisted and then silently discarded on the next launch —
195
- // you came back to Events having chosen otherwise.
196
- if (storedTab === "routes" || storedTab === "events" || storedTab === "stack") {
197
- setActiveTab(storedTab);
198
- }
199
- hasLoadedTabState.current = true;
200
- } catch (error) {
201
- // Failed to load tab state
202
- }
203
- };
204
- loadTabState();
205
- }, [visible]);
206
-
207
- // Load persisted monitoring state on mount
208
- (0, _react.useEffect)(() => {
209
- if (!visible || hasLoadedMonitoringState.current) return;
210
- const loadMonitoringState = async () => {
211
- try {
212
- const storedMonitoring = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring());
213
- if (storedMonitoring !== null) {
214
- const shouldMonitor = storedMonitoring === "true";
215
- setIsListeningEnabled(shouldMonitor);
216
- }
217
- hasLoadedMonitoringState.current = true;
218
- } catch (error) {
219
- // Failed to load monitoring state
220
- }
221
- };
222
- loadMonitoringState();
223
- }, [visible]);
224
-
225
- // Save tab state when it changes
226
- (0, _react.useEffect)(() => {
227
- if (!hasLoadedTabState.current) return;
228
- const saveTabState = async () => {
229
- try {
230
- await _sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.routeEvents.activeTab(), activeTab);
231
- } catch (error) {
232
- // Failed to save tab state
233
- }
234
- };
235
- saveTabState();
236
- }, [activeTab]);
237
-
238
- // Save monitoring state when it changes
239
- (0, _react.useEffect)(() => {
240
- if (!hasLoadedMonitoringState.current) return;
241
- const saveMonitoringState = async () => {
242
- try {
243
- await _sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring(), isListening.toString());
244
- } catch (error) {
245
- // Failed to save monitoring state
246
- }
247
- };
248
- saveMonitoringState();
249
- }, [isListening]);
250
-
251
- // Load persisted filters on mount
252
- (0, _react.useEffect)(() => {
253
- if (!visible || hasLoadedFilters.current) return;
254
- const loadFilters = async () => {
255
- try {
256
- const storedFilters = await _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.routeEvents.eventFilters());
257
- if (storedFilters) {
258
- const filters = JSON.parse(storedFilters);
259
- setIgnoredPatterns(new Set(filters));
260
- }
261
- hasLoadedFilters.current = true;
262
- } catch (error) {
263
- // Failed to load filters
264
- }
265
- };
266
- loadFilters();
267
- }, [visible]);
268
-
269
- // Save filters when they change
270
- (0, _react.useEffect)(() => {
271
- if (!hasLoadedFilters.current) return;
272
- const saveFilters = async () => {
273
- try {
274
- const filters = Array.from(ignoredPatterns);
275
- await _sharedUi.persistentStorage.setItem(_sharedUi.devToolsStorageKeys.routeEvents.eventFilters(), JSON.stringify(filters));
276
- } catch (error) {
277
- // Failed to save filters
278
- }
279
- };
280
- saveFilters();
281
- }, [ignoredPatterns]);
282
-
283
200
  // Event listener is now managed by useRouteEvents hook
284
201
 
285
202
  const handleToggleListening = (0, _react.useCallback)(() => {
@@ -73,17 +73,8 @@ function RouteEventsTimeline({
73
73
  onPress: handleItemPress,
74
74
  onNavigate: onNavigate
75
75
  }),
76
- showsVerticalScrollIndicator: true
77
- // Clipped cells fail Detox's hittability check even when they are on
78
- // screen, so the e2e bundle keeps every mounted row targetable. The
79
- // env var is inlined at bundle time; guarded so a non-inlining bundler
80
- // keeps production behaviour.
81
- ,
82
- removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
83
- initialNumToRender: 12,
84
- maxToRenderPerBatch: 8,
85
- windowSize: 5,
86
- updateCellsBatchingPeriod: 50
76
+ showsVerticalScrollIndicator: true,
77
+ ..._sharedUi.EVENT_LIST_PRESET
87
78
  })
88
79
  );
89
80
  }
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.useRouteEvents = useRouteEvents;
7
- var _react = require("react");
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
8
  var _routeEventStore = require("../stores/routeEventStore");
9
9
  /**
10
10
  * useRouteEvents Hook
@@ -39,50 +39,19 @@ function useRouteEvents(options = {}) {
39
39
  maxEvents = 500,
40
40
  enabled = true
41
41
  } = options;
42
- const [events, setEvents] = (0, _react.useState)([]);
43
- // Track listening state locally so it updates reactively with the subscription
44
- const [isListening, setIsListening] = (0, _react.useState)(enabled);
45
-
46
- // Subscribe to store - this automatically starts/stops listening
47
- // based on subscriber count (Subscribable pattern from TanStack Query)
48
- // Only subscribe when enabled is true
49
- (0, _react.useEffect)(() => {
50
- if (!enabled) {
51
- setIsListening(false);
52
- return;
53
- }
54
-
55
- // Frame-coalesced (see network's useNetworkEventList): navigation
56
- // bursts land as one commit per frame instead of one per emission.
57
- let rafId = null;
58
- let pending = null;
59
- const unsubscribe = _routeEventStore.routeEventStore.subscribeToEvents(allEvents => {
60
- pending = allEvents;
61
- if (rafId != null) return;
62
- rafId = requestAnimationFrame(() => {
63
- rafId = null;
64
- if (!pending) return;
65
- const latest = pending;
66
- pending = null;
67
- setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
68
- });
69
- });
70
-
71
- // Mark as listening after successful subscription
72
- setIsListening(true);
73
- return () => {
74
- unsubscribe();
75
- if (rafId != null) cancelAnimationFrame(rafId);
76
- // Note: Don't set isListening=false here because we might be
77
- // re-subscribing due to dependency change, not actually stopping
78
- };
79
- }, [maxEvents, enabled]);
80
- const clearEvents = (0, _react.useCallback)(() => {
81
- _routeEventStore.routeEventStore.clearEvents();
82
- }, []);
42
+ // Subscribing auto-starts the route observer (Subscribable pattern), and
43
+ // the shared hook frame-coalesces navigation bursts into one commit.
44
+ const {
45
+ events,
46
+ clearEvents,
47
+ isActive
48
+ } = (0, _sharedUi.useEventStoreEvents)(_routeEventStore.routeEventStore, {
49
+ maxEvents,
50
+ enabled
51
+ });
83
52
  return {
84
53
  events,
85
54
  clearEvents,
86
- isListening
55
+ isListening: isActive
87
56
  };
88
57
  }
@@ -3,10 +3,10 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.createRouteEventsTool = createRouteEventsTool;
7
- exports.routeEventsToolPreset = void 0;
6
+ exports.routeEventsToolPreset = exports.createRouteEventsTool = void 0;
8
7
  var _floatingToolsCore = require("@buoy-gg/floating-tools-core");
9
8
  var _RouteEventsModalWithTabs = require("./components/RouteEventsModalWithTabs");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _jsxRuntime = require("react/jsx-runtime");
11
11
  /**
12
12
  * Pre-configured route events tool for FloatingDevTools
@@ -33,7 +33,7 @@ var _jsxRuntime = require("react/jsx-runtime");
33
33
  * - Navigation stack visualization
34
34
  * - Automatic route tracking (no setup needed)
35
35
  */
36
- const routeEventsToolPreset = exports.routeEventsToolPreset = {
36
+ const routeEventsToolPreset = exports.routeEventsToolPreset = (0, _sharedUi.defineToolPreset)({
37
37
  id: "route-events",
38
38
  name: "ROUTES",
39
39
  description: "Route tracking & navigation inspector",
@@ -47,7 +47,7 @@ const routeEventsToolPreset = exports.routeEventsToolPreset = {
47
47
  props: {
48
48
  enableSharedModalDimensions: false
49
49
  }
50
- };
50
+ });
51
51
 
52
52
  /**
53
53
  * Create a custom route events tool configuration.
@@ -64,20 +64,6 @@ const routeEventsToolPreset = exports.routeEventsToolPreset = {
64
64
  * });
65
65
  * ```
66
66
  */
67
- function createRouteEventsTool(options) {
68
- return {
69
- id: options?.id || "route-events",
70
- name: options?.name || "ROUTES",
71
- description: options?.description || "Route tracking & navigation inspector",
72
- slot: "both",
73
- icon: ({
74
- size
75
- }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.RoutesIcon, {
76
- size: size
77
- }),
78
- component: _RouteEventsModalWithTabs.RouteEventsModalWithTabs,
79
- props: {
80
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
81
- }
82
- };
83
- }
67
+ const createRouteEventsTool = exports.createRouteEventsTool = (0, _sharedUi.createToolFactory)(routeEventsToolPreset, options => ({
68
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
69
+ }));
@@ -8,8 +8,8 @@ import { getCommonActions, useSafeNavigationContainerRef } from "../utils/safeRe
8
8
  // (useSafeNavigationContainerRef then yields undefined, so the dispatch
9
9
  // paths below never execute).
10
10
  const CommonActions = getCommonActions();
11
- import { useSafeRouter, isExpoRouterAvailable, windowDragProps, ToolEmptyState, resolveListEmptyState } from "@buoy-gg/shared-ui";
12
- import { JsModal, ModalHeader, TabSelector, formatRelativeTime, devToolsStorageKeys, Trash2, Filter, SearchBar, persistentStorage, ToolbarCopyButton, Lock, ProUpgradeModal, PowerToggleButton, buoyColors, night, TickProvider, useLimit } from "@buoy-gg/shared-ui";
11
+ import { useSafeRouter, isExpoRouterAvailable, windowDragProps, ToolEmptyState, resolveListEmptyState, usePersistedState } 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
13
  import { useIsPro } from "@buoy-gg/license";
14
14
 
15
15
  // Free tier limit for route events
@@ -22,6 +22,23 @@ import { ReactNavigationRoutes } from "./ReactNavigationRoutes";
22
22
  import { NavigationStack } from "./NavigationStack";
23
23
  import { RouteEventsTimeline } from "./RouteEventsTimeline";
24
24
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
25
+ /** Persistence recipes: the tab is stored as its bare name, the flag as "true"/"false". */
26
+ const TAB_PERSISTENCE = {
27
+ // Every tab the selector offers, including "stack": the save side has
28
+ // always written it, so leaving it off this list meant the Stack tab
29
+ // was persisted and then silently discarded on the next launch.
30
+ parse: raw => raw === "routes" || raw === "events" || raw === "stack" ? raw : undefined,
31
+ serialize: tab => tab
32
+ };
33
+ const BOOLEAN_PERSISTENCE = {
34
+ parse: raw => raw === "true",
35
+ serialize: value => value.toString()
36
+ };
37
+ const STRING_SET_PERSISTENCE = {
38
+ parse: raw => new Set(JSON.parse(raw)),
39
+ serialize: set => JSON.stringify(Array.from(set))
40
+ };
41
+
25
42
  /**
26
43
  * Find the path from root navigation state to a screen by name.
27
44
  * Returns array of route names from root to the target, or null if not found.
@@ -82,7 +99,7 @@ export function RouteEventsModalWithTabs({
82
99
  const router = useSafeRouter();
83
100
  const containerRef = useSafeNavigationContainerRef();
84
101
  const hasExpoRouter = isExpoRouterAvailable();
85
- const [activeTab, setActiveTab] = useState("events");
102
+ const [activeTab, setActiveTab] = usePersistedState(devToolsStorageKeys.routeEvents.activeTab(), "events", TAB_PERSISTENCE);
86
103
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
87
104
  // Serialized stack reported up by NavigationStack so the copy button can live
88
105
  // in the shared navbar.
@@ -104,7 +121,7 @@ export function RouteEventsModalWithTabs({
104
121
  // See the RouteTracker component export for easy setup.
105
122
 
106
123
  // Local state to control subscription (true = subscribed, false = unsubscribed)
107
- const [isListeningEnabled, setIsListeningEnabled] = useState(true);
124
+ const [isListeningEnabled, setIsListeningEnabled] = usePersistedState(devToolsStorageKeys.routeEvents.isMonitoring(), true, BOOLEAN_PERSISTENCE);
108
125
 
109
126
  // Event Listener state - using centralized store via hook
110
127
  // Pass enabled option to control subscription
@@ -124,11 +141,8 @@ export function RouteEventsModalWithTabs({
124
141
  const [selectedEvent, setSelectedEvent] = useState(null);
125
142
  const [selectedVisitNumber, setSelectedVisitNumber] = useState(1);
126
143
  const [searchQuery, setSearchQuery] = useState("");
127
- const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["/_sitemap", "/api", "/__dev"]));
144
+ const [ignoredPatterns, setIgnoredPatterns] = usePersistedState(devToolsStorageKeys.routeEvents.eventFilters(), new Set(["/_sitemap", "/api", "/__dev"]), STRING_SET_PERSISTENCE);
128
145
  const lastEventRef = useRef(null);
129
- const hasLoadedFilters = useRef(false);
130
- const hasLoadedTabState = useRef(false);
131
- const hasLoadedMonitoringState = useRef(false);
132
146
  const handleModeChange = useCallback(_mode => {
133
147
  // Mode changes handled by JsModal
134
148
  }, []);
@@ -180,103 +194,6 @@ export function RouteEventsModalWithTabs({
180
194
  }
181
195
  }, [router, containerRef, hasExpoRouter, remote, isPro]);
182
196
 
183
- // Load persisted tab state on mount
184
- useEffect(() => {
185
- if (!visible || hasLoadedTabState.current) return;
186
- const loadTabState = async () => {
187
- try {
188
- const storedTab = await persistentStorage.getItem(devToolsStorageKeys.routeEvents.activeTab());
189
- // Every tab the selector offers, including "stack": the save side has
190
- // always written it, so leaving it off this list meant the Stack tab
191
- // was persisted and then silently discarded on the next launch —
192
- // you came back to Events having chosen otherwise.
193
- if (storedTab === "routes" || storedTab === "events" || storedTab === "stack") {
194
- setActiveTab(storedTab);
195
- }
196
- hasLoadedTabState.current = true;
197
- } catch (error) {
198
- // Failed to load tab state
199
- }
200
- };
201
- loadTabState();
202
- }, [visible]);
203
-
204
- // Load persisted monitoring state on mount
205
- useEffect(() => {
206
- if (!visible || hasLoadedMonitoringState.current) return;
207
- const loadMonitoringState = async () => {
208
- try {
209
- const storedMonitoring = await persistentStorage.getItem(devToolsStorageKeys.routeEvents.isMonitoring());
210
- if (storedMonitoring !== null) {
211
- const shouldMonitor = storedMonitoring === "true";
212
- setIsListeningEnabled(shouldMonitor);
213
- }
214
- hasLoadedMonitoringState.current = true;
215
- } catch (error) {
216
- // Failed to load monitoring state
217
- }
218
- };
219
- loadMonitoringState();
220
- }, [visible]);
221
-
222
- // Save tab state when it changes
223
- useEffect(() => {
224
- if (!hasLoadedTabState.current) return;
225
- const saveTabState = async () => {
226
- try {
227
- await persistentStorage.setItem(devToolsStorageKeys.routeEvents.activeTab(), activeTab);
228
- } catch (error) {
229
- // Failed to save tab state
230
- }
231
- };
232
- saveTabState();
233
- }, [activeTab]);
234
-
235
- // Save monitoring state when it changes
236
- useEffect(() => {
237
- if (!hasLoadedMonitoringState.current) return;
238
- const saveMonitoringState = async () => {
239
- try {
240
- await persistentStorage.setItem(devToolsStorageKeys.routeEvents.isMonitoring(), isListening.toString());
241
- } catch (error) {
242
- // Failed to save monitoring state
243
- }
244
- };
245
- saveMonitoringState();
246
- }, [isListening]);
247
-
248
- // Load persisted filters on mount
249
- useEffect(() => {
250
- if (!visible || hasLoadedFilters.current) return;
251
- const loadFilters = async () => {
252
- try {
253
- const storedFilters = await persistentStorage.getItem(devToolsStorageKeys.routeEvents.eventFilters());
254
- if (storedFilters) {
255
- const filters = JSON.parse(storedFilters);
256
- setIgnoredPatterns(new Set(filters));
257
- }
258
- hasLoadedFilters.current = true;
259
- } catch (error) {
260
- // Failed to load filters
261
- }
262
- };
263
- loadFilters();
264
- }, [visible]);
265
-
266
- // Save filters when they change
267
- useEffect(() => {
268
- if (!hasLoadedFilters.current) return;
269
- const saveFilters = async () => {
270
- try {
271
- const filters = Array.from(ignoredPatterns);
272
- await persistentStorage.setItem(devToolsStorageKeys.routeEvents.eventFilters(), JSON.stringify(filters));
273
- } catch (error) {
274
- // Failed to save filters
275
- }
276
- };
277
- saveFilters();
278
- }, [ignoredPatterns]);
279
-
280
197
  // Event listener is now managed by useRouteEvents hook
281
198
 
282
199
  const handleToggleListening = useCallback(() => {
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { useCallback } from "react";
14
14
  import { View, Text, FlatList, StyleSheet } from "react-native";
15
- import { Navigation, night } from "@buoy-gg/shared-ui";
15
+ import { Navigation, night, EVENT_LIST_PRESET } from "@buoy-gg/shared-ui";
16
16
  import { RouteEventItemCompact } from "./RouteEventItemCompact";
17
17
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  export function RouteEventsTimeline({
@@ -69,17 +69,8 @@ export function RouteEventsTimeline({
69
69
  onPress: handleItemPress,
70
70
  onNavigate: onNavigate
71
71
  }),
72
- showsVerticalScrollIndicator: true
73
- // Clipped cells fail Detox's hittability check even when they are on
74
- // screen, so the e2e bundle keeps every mounted row targetable. The
75
- // env var is inlined at bundle time; guarded so a non-inlining bundler
76
- // keeps production behaviour.
77
- ,
78
- removeClippedSubviews: !(typeof process !== "undefined" && process.env?.EXPO_PUBLIC_E2E),
79
- initialNumToRender: 12,
80
- maxToRenderPerBatch: 8,
81
- windowSize: 5,
82
- updateCellsBatchingPeriod: 50
72
+ showsVerticalScrollIndicator: true,
73
+ ...EVENT_LIST_PRESET
83
74
  })
84
75
  );
85
76
  }
@@ -26,7 +26,7 @@
26
26
  * ```
27
27
  */
28
28
 
29
- import { useState, useEffect, useCallback } from "react";
29
+ import { useEventStoreEvents } from "@buoy-gg/shared-ui";
30
30
  import { routeEventStore } from "../stores/routeEventStore";
31
31
 
32
32
  // Re-export RouteChangeEvent for convenience
@@ -36,50 +36,19 @@ export function useRouteEvents(options = {}) {
36
36
  maxEvents = 500,
37
37
  enabled = true
38
38
  } = options;
39
- const [events, setEvents] = useState([]);
40
- // Track listening state locally so it updates reactively with the subscription
41
- const [isListening, setIsListening] = useState(enabled);
42
-
43
- // Subscribe to store - this automatically starts/stops listening
44
- // based on subscriber count (Subscribable pattern from TanStack Query)
45
- // Only subscribe when enabled is true
46
- useEffect(() => {
47
- if (!enabled) {
48
- setIsListening(false);
49
- return;
50
- }
51
-
52
- // Frame-coalesced (see network's useNetworkEventList): navigation
53
- // bursts land as one commit per frame instead of one per emission.
54
- let rafId = null;
55
- let pending = null;
56
- const unsubscribe = routeEventStore.subscribeToEvents(allEvents => {
57
- pending = allEvents;
58
- if (rafId != null) return;
59
- rafId = requestAnimationFrame(() => {
60
- rafId = null;
61
- if (!pending) return;
62
- const latest = pending;
63
- pending = null;
64
- setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
65
- });
66
- });
67
-
68
- // Mark as listening after successful subscription
69
- setIsListening(true);
70
- return () => {
71
- unsubscribe();
72
- if (rafId != null) cancelAnimationFrame(rafId);
73
- // Note: Don't set isListening=false here because we might be
74
- // re-subscribing due to dependency change, not actually stopping
75
- };
76
- }, [maxEvents, enabled]);
77
- const clearEvents = useCallback(() => {
78
- routeEventStore.clearEvents();
79
- }, []);
39
+ // Subscribing auto-starts the route observer (Subscribable pattern), and
40
+ // the shared hook frame-coalesces navigation bursts into one commit.
41
+ const {
42
+ events,
43
+ clearEvents,
44
+ isActive
45
+ } = useEventStoreEvents(routeEventStore, {
46
+ maxEvents,
47
+ enabled
48
+ });
80
49
  return {
81
50
  events,
82
51
  clearEvents,
83
- isListening
52
+ isListening: isActive
84
53
  };
85
54
  }
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { RoutesIcon } from "@buoy-gg/floating-tools-core";
21
21
  import { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs";
22
+ import { createToolFactory, defineToolPreset } from "@buoy-gg/shared-ui";
22
23
 
23
24
  /**
24
25
  * Pre-configured route events tool for FloatingDevTools.
@@ -29,7 +30,7 @@ import { RouteEventsModalWithTabs } from "./components/RouteEventsModalWithTabs"
29
30
  * - Automatic route tracking (no setup needed)
30
31
  */
31
32
  import { jsx as _jsx } from "react/jsx-runtime";
32
- export const routeEventsToolPreset = {
33
+ export const routeEventsToolPreset = defineToolPreset({
33
34
  id: "route-events",
34
35
  name: "ROUTES",
35
36
  description: "Route tracking & navigation inspector",
@@ -43,7 +44,7 @@ export const routeEventsToolPreset = {
43
44
  props: {
44
45
  enableSharedModalDimensions: false
45
46
  }
46
- };
47
+ });
47
48
 
48
49
  /**
49
50
  * Create a custom route events tool configuration.
@@ -60,20 +61,6 @@ export const routeEventsToolPreset = {
60
61
  * });
61
62
  * ```
62
63
  */
63
- export function createRouteEventsTool(options) {
64
- return {
65
- id: options?.id || "route-events",
66
- name: options?.name || "ROUTES",
67
- description: options?.description || "Route tracking & navigation inspector",
68
- slot: "both",
69
- icon: ({
70
- size
71
- }) => /*#__PURE__*/_jsx(RoutesIcon, {
72
- size: size
73
- }),
74
- component: RouteEventsModalWithTabs,
75
- props: {
76
- enableSharedModalDimensions: options?.enableSharedModalDimensions !== undefined ? options.enableSharedModalDimensions : false
77
- }
78
- };
79
- }
64
+ export const createRouteEventsTool = createToolFactory(routeEventsToolPreset, options => ({
65
+ enableSharedModalDimensions: options.enableSharedModalDimensions ?? false
66
+ }));
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventsModalWithTabs.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsModalWithTabs.tsx"],"names":[],"mappings":"AAyDA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAGnE,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE;YACR,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;YAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;SAC/B,CAAC;QACF,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC3B;;;;WAIG;QACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;QAC7C;;;;;WAKG;QACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5C,2EAA2E;QAC3E,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,yBAAyB,EACjC,OAAO,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KACzB,IAAI,CAAC;KACX,CAAC;CACH;AA8CD,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,EACd,MAAM,GACP,EAAE,6BAA6B,sCA8tB/B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,GAAE,IAAI,CAAC,6BAA6B,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAKlF"}
1
+ {"version":3,"file":"RouteEventsModalWithTabs.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsModalWithTabs.tsx"],"names":[],"mappings":"AAyDA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAGnE,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE;YACR,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;YAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;SAC/B,CAAC;QACF,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC3B;;;;WAIG;QACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;QAC7C;;;;;WAKG;QACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5C,2EAA2E;QAC3E,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,yBAAyB,EACjC,OAAO,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KACzB,IAAI,CAAC;KACX,CAAC;CACH;AAgED,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,EACd,MAAM,GACP,EAAE,6BAA6B,sCAqmB/B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,GAAE,IAAI,CAAC,6BAA6B,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAKlF"}
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B;wEACoE;IACpE,WAAW,EAAE,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACvE;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,EACV,aAAa,GACd,EAAE,wBAAwB,+BA8D1B"}
1
+ {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B;wEACoE;IACpE,WAAW,EAAE,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;CACvE;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,EACV,aAAa,GACd,EAAE,wBAAwB,+BAoD1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAoDtB"}
1
+ {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAStB"}
@@ -51,16 +51,10 @@ export declare const routeEventsToolPreset: {
51
51
  * });
52
52
  * ```
53
53
  */
54
- export declare function createRouteEventsTool(options?: {
55
- /** Tool name (default: "ROUTES") */
56
- name?: string;
57
- /** Tool description */
58
- description?: string;
59
- /** Custom tool ID (default: "route-events") */
60
- id?: string;
54
+ export declare const createRouteEventsTool: (options?: ({
61
55
  /** Enable shared modal dimensions */
62
56
  enableSharedModalDimensions?: boolean;
63
- }): {
57
+ } & import("@buoy-gg/shared-ui").ToolPresetOverrides) | undefined) => {
64
58
  id: string;
65
59
  name: string;
66
60
  description: string;
@@ -1 +1 @@
1
- {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAEjF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;qBAKf;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;CAKlC,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE;IAC9C,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;;;;;qBAOoB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;EASpC"}
1
+ {"version":3,"file":"preset.d.ts","sourceRoot":"","sources":["../../src/preset.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,wBAAwB,EAAE,MAAM,uCAAuC,CAAC;AAGjF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;qBAKf;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;CAKjC,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB;IAG9B,qCAAqC;kCACP,OAAO;;;;;;qBA1BtB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;;;;CA8BlC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/route-events",
3
- "version": "7.0.33",
3
+ "version": "7.0.34",
4
4
  "description": "Navigation devtool for React Native — live stack, full sitemap, jump to any screen. Expo Router & React Navigation. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,11 +26,11 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/floating-tools-core": "^7.0.33",
30
- "@buoy-gg/shared-ui": "^7.0.33"
29
+ "@buoy-gg/shared-ui": "^7.0.34",
30
+ "@buoy-gg/floating-tools-core": "^7.0.34"
31
31
  },
32
32
  "peerDependencies": {
33
- "@buoy-gg/license": "7.0.33",
33
+ "@buoy-gg/license": "7.0.34",
34
34
  "@react-native-async-storage/async-storage": "^2.0.0 || ^3.1.0",
35
35
  "@react-navigation/native": "*",
36
36
  "expo-router": "*",
@@ -52,7 +52,7 @@
52
52
  "@types/react-native": "^0.73.0",
53
53
  "expo-router": "~5.0.7",
54
54
  "typescript": "~5.8.3",
55
- "@buoy-gg/license": "^7.0.33"
55
+ "@buoy-gg/license": "^7.0.34"
56
56
  },
57
57
  "react-native-builder-bob": {
58
58
  "source": "src",