@buoy-gg/route-events 3.0.1 → 4.0.1

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 (35) hide show
  1. package/lib/commonjs/RouteTracker.js +38 -4
  2. package/lib/commonjs/components/NavigationStack.js +105 -124
  3. package/lib/commonjs/components/RouteEventsModalWithTabs.js +11 -4
  4. package/lib/commonjs/components/RoutesSitemap.js +205 -169
  5. package/lib/commonjs/expoRouterStore.js +17 -0
  6. package/lib/commonjs/index.js +7 -0
  7. package/lib/commonjs/stores/navigationStackStore.js +45 -0
  8. package/lib/commonjs/sync/routeEventsSyncAdapter.js +148 -0
  9. package/lib/commonjs/useRouteSitemap.js +28 -7
  10. package/lib/module/RouteTracker.js +39 -5
  11. package/lib/module/components/NavigationStack.js +106 -125
  12. package/lib/module/components/RouteEventsModalWithTabs.js +11 -4
  13. package/lib/module/components/RoutesSitemap.js +207 -171
  14. package/lib/module/expoRouterStore.js +16 -0
  15. package/lib/module/index.js +4 -0
  16. package/lib/module/stores/navigationStackStore.js +41 -0
  17. package/lib/module/sync/routeEventsSyncAdapter.js +145 -0
  18. package/lib/module/useRouteSitemap.js +29 -8
  19. package/lib/typescript/RouteTracker.d.ts.map +1 -1
  20. package/lib/typescript/components/NavigationStack.d.ts +24 -1
  21. package/lib/typescript/components/NavigationStack.d.ts.map +1 -1
  22. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
  23. package/lib/typescript/components/RoutesSitemap.d.ts +19 -1
  24. package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -1
  25. package/lib/typescript/expoRouterStore.d.ts +8 -0
  26. package/lib/typescript/expoRouterStore.d.ts.map +1 -1
  27. package/lib/typescript/index.d.ts +3 -1
  28. package/lib/typescript/index.d.ts.map +1 -1
  29. package/lib/typescript/stores/navigationStackStore.d.ts +33 -0
  30. package/lib/typescript/stores/navigationStackStore.d.ts.map +1 -0
  31. package/lib/typescript/sync/routeEventsSyncAdapter.d.ts +69 -0
  32. package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +1 -0
  33. package/lib/typescript/useRouteSitemap.d.ts +17 -0
  34. package/lib/typescript/useRouteSitemap.d.ts.map +1 -1
  35. package/package.json +6 -6
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.routeEventsSyncAdapter = void 0;
7
+ var _sharedUi = require("@buoy-gg/shared-ui");
8
+ var _routeEventStore = require("../stores/routeEventStore");
9
+ var _navigationStackStore = require("../stores/navigationStackStore");
10
+ var _RouteParser = require("../RouteParser");
11
+ var _expoRouterStore = require("../expoRouterStore");
12
+ /**
13
+ * Serializable snapshot of the device's route tree, sent to the dashboard so
14
+ * the Routes tab can render a sitemap (the raw expo-router RouteNode itself is
15
+ * not JSON-serializable — it holds components/functions — so we send the parsed
16
+ * RouteInfo[] instead).
17
+ */
18
+
19
+ /**
20
+ * Payload shape for the route-events tool (adapter version 3). Consumers must
21
+ * tolerate older shapes: v1 sends a bare RouteChangeEvent[]; v2 omits `stack`.
22
+ */
23
+
24
+ function getSitemapSnapshot() {
25
+ const metadata = (0, _expoRouterStore.getRouteNodeMetadata)();
26
+ return {
27
+ routes: _RouteParser.RouteParser.parseRouteTree((0, _expoRouterStore.loadRouteNode)()),
28
+ source: metadata.source,
29
+ lastUpdatedAt: metadata.lastLoadedAt
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Sync adapter for the route-events tool, consumed by @buoy-gg/external-sync's
35
+ * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
36
+ * this package doesn't need a dependency on it).
37
+ *
38
+ * Subscribing attaches the routeObserver listener, so route changes are only
39
+ * recorded while a dashboard is watching. (Route events still require a
40
+ * <RouteTracker /> inside the navigation tree to be emitted at all.)
41
+ *
42
+ * The snapshot carries both the route-change events AND the parsed route
43
+ * sitemap, so the dashboard — which has no local expo-router store — can render
44
+ * the route tree. The `navigate` action lets the dashboard drive navigation on
45
+ * the device.
46
+ */
47
+ const routeEventsSyncAdapter = exports.routeEventsSyncAdapter = {
48
+ version: 3,
49
+ getSnapshot: () => ({
50
+ events: _routeEventStore.routeEventStore.getEvents(),
51
+ sitemap: getSitemapSnapshot(),
52
+ stack: _navigationStackStore.navigationStackStore.getStack()
53
+ }),
54
+ subscribe: onChange => {
55
+ const unsubscribeEvents = _routeEventStore.routeEventStore.subscribeToEvents(onChange);
56
+ const unsubscribeStack = _navigationStackStore.navigationStackStore.subscribe(onChange);
57
+
58
+ // The expo-router route tree can still be populating when a dashboard
59
+ // starts watching. Poll briefly until it's available and push a snapshot
60
+ // the moment it loads, so the Routes tab fills in without needing the user
61
+ // to navigate first. Stops as soon as the tree is found.
62
+ let pollTimer = null;
63
+ if (!(0, _expoRouterStore.loadRouteNode)()) {
64
+ let retries = 0;
65
+ const maxRetries = 100; // ~10s
66
+ const poll = () => {
67
+ retries += 1;
68
+ if ((0, _expoRouterStore.loadRouteNode)()) {
69
+ pollTimer = null;
70
+ onChange();
71
+ return;
72
+ }
73
+ pollTimer = retries < maxRetries ? setTimeout(poll, 100) : null;
74
+ };
75
+ pollTimer = setTimeout(poll, 100);
76
+ }
77
+ return () => {
78
+ unsubscribeEvents();
79
+ unsubscribeStack();
80
+ if (pollTimer) clearTimeout(pollTimer);
81
+ };
82
+ },
83
+ actions: {
84
+ clearEvents: () => {
85
+ _routeEventStore.routeEventStore.clearEvents();
86
+ },
87
+ /**
88
+ * Navigate the device to a concrete path. The dashboard resolves any
89
+ * dynamic params into a concrete path before invoking this.
90
+ */
91
+ navigate: params => {
92
+ const path = params?.path;
93
+ if (!path) {
94
+ throw new Error("navigate requires a 'path' param");
95
+ }
96
+ const router = (0, _sharedUi.getSafeRouter)();
97
+ if (!router) {
98
+ throw new Error("expo-router is not available on this device");
99
+ }
100
+ router.navigate(path);
101
+ return {
102
+ navigated: path
103
+ };
104
+ },
105
+ // ── Stack actions: delegate to the live navigation actions captured by
106
+ // <RouteTracker /> (they hold the React Navigation container ref). ──
107
+ stackNavigateToIndex: params => {
108
+ const index = params?.index;
109
+ if (typeof index !== "number") {
110
+ throw new Error("stackNavigateToIndex requires a numeric 'index'");
111
+ }
112
+ const actions = _navigationStackStore.navigationStackStore.getActions();
113
+ if (!actions) throw new Error("navigation stack is not available");
114
+ actions.navigateToIndex(index);
115
+ return {
116
+ navigatedToIndex: index
117
+ };
118
+ },
119
+ stackPopToIndex: params => {
120
+ const index = params?.index;
121
+ if (typeof index !== "number") {
122
+ throw new Error("stackPopToIndex requires a numeric 'index'");
123
+ }
124
+ const actions = _navigationStackStore.navigationStackStore.getActions();
125
+ if (!actions) throw new Error("navigation stack is not available");
126
+ actions.popToIndex(index);
127
+ return {
128
+ poppedToIndex: index
129
+ };
130
+ },
131
+ stackGoBack: () => {
132
+ const actions = _navigationStackStore.navigationStackStore.getActions();
133
+ if (!actions) throw new Error("navigation stack is not available");
134
+ actions.goBack();
135
+ return {
136
+ wentBack: true
137
+ };
138
+ },
139
+ stackPopToTop: () => {
140
+ const actions = _navigationStackStore.navigationStackStore.getActions();
141
+ if (!actions) throw new Error("navigation stack is not available");
142
+ actions.popToTop();
143
+ return {
144
+ poppedToTop: true
145
+ };
146
+ }
147
+ }
148
+ };
@@ -96,8 +96,12 @@ function useRouteSitemap(options = {}) {
96
96
  searchQuery = "",
97
97
  sortBy = "path",
98
98
  autoRefresh = false,
99
- refreshInterval = 1000
99
+ refreshInterval = 1000,
100
+ injectedRoutes,
101
+ injectedSource = null,
102
+ injectedLastUpdatedAt = null
100
103
  } = options;
104
+ const hasInjectedRoutes = injectedRoutes != null;
101
105
  const [routeTreeState, setRouteTreeState] = (0, _react.useState)(() => {
102
106
  const node = (0, _expoRouterStore.loadRouteNode)();
103
107
  const metadata = (0, _expoRouterStore.getRouteNodeMetadata)();
@@ -132,6 +136,16 @@ function useRouteSitemap(options = {}) {
132
136
  if (routeNode) {
133
137
  return;
134
138
  }
139
+
140
+ // Routes injected from outside (dashboard) don't come from the local store.
141
+ if (hasInjectedRoutes) {
142
+ return;
143
+ }
144
+
145
+ // On web (no expo-router) the tree will never load — don't poll forever.
146
+ if (!(0, _expoRouterStore.isExpoRouterStoreSupported)()) {
147
+ return;
148
+ }
135
149
  let retryCount = 0;
136
150
  const maxRetries = 100; // 10 seconds max
137
151
 
@@ -144,7 +158,7 @@ function useRouteSitemap(options = {}) {
144
158
  };
145
159
  let timeoutRef = setTimeout(poll, 100);
146
160
  return () => clearTimeout(timeoutRef);
147
- }, [routeNode, refresh]);
161
+ }, [routeNode, refresh, hasInjectedRoutes]);
148
162
 
149
163
  // Optional auto-refresh hook for callers that want periodic updates
150
164
  (0, _react.useEffect)(() => {
@@ -152,13 +166,19 @@ function useRouteSitemap(options = {}) {
152
166
  const interval = setInterval(refresh, refreshInterval);
153
167
  return () => clearInterval(interval);
154
168
  }, [autoRefresh, refreshInterval, refresh]);
155
- const isLoaded = !!routeNode;
156
169
 
157
- // Parse routes
170
+ // With injected routes we're "supported" regardless of the local runtime, and
171
+ // "loaded" once the device has actually sent a non-empty tree (an empty array
172
+ // means the device is still booting expo-router → keep showing "Loading...").
173
+ const isLoaded = hasInjectedRoutes ? injectedRoutes.length > 0 : !!routeNode;
174
+ const isSupported = hasInjectedRoutes ? true : (0, _expoRouterStore.isExpoRouterStoreSupported)();
175
+
176
+ // Parse routes (or use the pre-parsed routes injected from the dashboard).
158
177
  const routes = (0, _react.useMemo)(() => {
178
+ if (hasInjectedRoutes) return injectedRoutes;
159
179
  if (!routeNode) return [];
160
180
  return _RouteParser.RouteParser.parseRouteTree(routeNode);
161
- }, [routeNode, routeNodeVersion]);
181
+ }, [routeNode, routeNodeVersion, hasInjectedRoutes, injectedRoutes]);
162
182
 
163
183
  // Sort routes
164
184
  const sortedRoutes = (0, _react.useMemo)(() => {
@@ -190,11 +210,12 @@ function useRouteSitemap(options = {}) {
190
210
  stats,
191
211
  filteredRoutes,
192
212
  isLoaded,
213
+ isSupported,
193
214
  refresh,
194
215
  findRoute,
195
216
  getParents,
196
- lastUpdatedAt,
197
- source
217
+ lastUpdatedAt: hasInjectedRoutes ? injectedLastUpdatedAt : lastUpdatedAt,
218
+ source: hasInjectedRoutes ? injectedSource : source
198
219
  };
199
220
  }
200
221
 
@@ -41,10 +41,13 @@
41
41
  * ```
42
42
  */
43
43
 
44
+ import { useEffect } from "react";
44
45
  import { isExpoRouterAvailable } from "@buoy-gg/shared-ui";
45
46
  import { useRouteObserver } from "./useRouteObserver";
46
47
  import { useRouteObserverReactNavigation } from "./useRouteObserverReactNavigation";
47
- import { jsx as _jsx } from "react/jsx-runtime";
48
+ import { useNavigationStack } from "./useNavigationStack";
49
+ import { navigationStackStore } from "./stores/navigationStackStore";
50
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
48
51
  function ExpoRouteTracker() {
49
52
  useRouteObserver();
50
53
  return null;
@@ -53,10 +56,41 @@ function ReactNavigationRouteTracker() {
53
56
  useRouteObserverReactNavigation();
54
57
  return null;
55
58
  }
59
+
60
+ /**
61
+ * Mirrors the live navigation stack (and its action functions) into
62
+ * navigationStackStore so the route-events sync adapter can expose the Stack
63
+ * tab to the dashboard. Runs inside the navigation tree where the container ref
64
+ * context is available.
65
+ */
66
+ function NavigationStackCapture() {
67
+ const {
68
+ stack,
69
+ navigateToIndex,
70
+ popToIndex,
71
+ goBack,
72
+ popToTop
73
+ } = useNavigationStack();
74
+ useEffect(() => {
75
+ navigationStackStore.setStack(stack);
76
+ }, [stack]);
77
+
78
+ // Keep the action references fresh (they change as the stack changes) so a
79
+ // remote action always operates on the current navigation state.
80
+ useEffect(() => {
81
+ navigationStackStore.setActions({
82
+ navigateToIndex,
83
+ popToIndex,
84
+ goBack,
85
+ popToTop
86
+ });
87
+ return () => navigationStackStore.setActions(null);
88
+ });
89
+ return null;
90
+ }
56
91
  export function RouteTracker() {
57
92
  const hasExpo = isExpoRouterAvailable();
58
- if (hasExpo) {
59
- return /*#__PURE__*/_jsx(ExpoRouteTracker, {});
60
- }
61
- return /*#__PURE__*/_jsx(ReactNavigationRouteTracker, {});
93
+ return /*#__PURE__*/_jsxs(_Fragment, {
94
+ children: [hasExpo ? /*#__PURE__*/_jsx(ExpoRouteTracker, {}) : /*#__PURE__*/_jsx(ReactNavigationRouteTracker, {}), /*#__PURE__*/_jsx(NavigationStackCapture, {})]
95
+ });
62
96
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { useState, useMemo, useEffect } from "react";
11
11
  import { View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert } from "react-native";
12
- import { ChevronDown, ChevronRight, Info, InlineCopyButton, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
12
+ import { ChevronDown, ChevronRight, InlineCopyButton, ProUpgradeModal, buoyColors, useSafeAreaInsets } from "@buoy-gg/shared-ui";
13
13
  import { useIsPro } from "@buoy-gg/license";
14
14
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
15
15
  import { useNavigationStack } from "../useNavigationStack";
@@ -17,27 +17,40 @@ import { useNavigationStack } from "../useNavigationStack";
17
17
  // ============================================================================
18
18
  // Types
19
19
  // ============================================================================
20
+
21
+ /** Stack manipulation a host can be asked to perform on the device. */
20
22
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
23
  // ============================================================================
22
24
  // Main Component
23
25
  // ============================================================================
24
26
 
25
27
  export function NavigationStack({
26
- style
28
+ style,
29
+ injectedStack,
30
+ onAction,
31
+ onCopyValueChange
27
32
  }) {
28
- const {
29
- stack,
30
- focusedRoute,
31
- stackDepth,
32
- isAtRoot,
33
- isLoaded,
34
- error,
35
- navigateToIndex,
36
- goBack,
37
- popToTop
38
- } = useNavigationStack();
33
+ const hookResult = useNavigationStack();
34
+ const isInjected = injectedStack != null;
35
+ const insets = useSafeAreaInsets({
36
+ minBottom: 8
37
+ });
38
+
39
+ // Data source: injected (dashboard) or the local navigation container.
40
+ const stack = isInjected ? injectedStack : hookResult.stack;
41
+ const isLoaded = isInjected ? true : hookResult.isLoaded;
42
+ const error = isInjected ? null : hookResult.error;
43
+ const focusedRoute = useMemo(() => stack.find(item => item.isFocused) ?? null, [stack]);
44
+ const stackDepth = stack.length;
45
+ const isAtRoot = stackDepth <= 1;
46
+
47
+ // Action wrappers: delegate to the host when provided, else act locally.
48
+ const navigateToIndex = index => onAction ? onAction("navigateToIndex", {
49
+ index
50
+ }) : hookResult.navigateToIndex(index);
51
+ const goBack = () => onAction ? onAction("goBack") : hookResult.goBack();
52
+ const popToTop = () => onAction ? onAction("popToTop") : hookResult.popToTop();
39
53
  const [expandedIndex, setExpandedIndex] = useState(null);
40
- const [showHelp, setShowHelp] = useState(false);
41
54
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
42
55
 
43
56
  // Check Pro status internally
@@ -62,6 +75,12 @@ export function NavigationStack({
62
75
  return JSON.stringify(stackData, null, 2);
63
76
  }, [stack]);
64
77
 
78
+ // Report the copy payload up so the host can render the copy button in the
79
+ // shared navbar.
80
+ useEffect(() => {
81
+ onCopyValueChange?.(stackDataForCopy);
82
+ }, [stackDataForCopy, onCopyValueChange]);
83
+
65
84
  // Determine which route actions should operate on
66
85
  // If a stack item is expanded, actions target that route
67
86
  // Otherwise, actions target the focused (visible) route
@@ -124,6 +143,26 @@ export function NavigationStack({
124
143
  }
125
144
 
126
145
  // Handlers
146
+ // Confirm a destructive action. On device we use the native Alert; when an
147
+ // action delegate is set (dashboard, react-native-web) Alert button presses
148
+ // don't fire, so fall back to window.confirm.
149
+ const confirmDestructive = (title, message, confirmLabel, onConfirm) => {
150
+ if (onAction) {
151
+ const confirmFn = globalThis.window?.confirm;
152
+ if (!confirmFn || confirmFn(`${title}\n\n${message}`)) {
153
+ onConfirm();
154
+ }
155
+ return;
156
+ }
157
+ Alert.alert(title, message, [{
158
+ text: "Cancel",
159
+ style: "cancel"
160
+ }, {
161
+ text: confirmLabel,
162
+ style: "destructive",
163
+ onPress: onConfirm
164
+ }]);
165
+ };
127
166
  const handleGoBack = () => {
128
167
  // Gate behind Pro
129
168
  if (!isPro) {
@@ -146,14 +185,7 @@ export function NavigationStack({
146
185
  Alert.alert("Already at Top", "Stack only has one screen");
147
186
  return;
148
187
  }
149
- Alert.alert("Pop to Top", "This will remove all screens except the root screen.", [{
150
- text: "Cancel",
151
- style: "cancel"
152
- }, {
153
- text: "Pop to Top",
154
- style: "destructive",
155
- onPress: popToTop
156
- }]);
188
+ confirmDestructive("Pop to Top", "This will remove all screens except the root screen.", "Pop to Top", popToTop);
157
189
  };
158
190
  const toggleExpand = index => {
159
191
  setExpandedIndex(expandedIndex === index ? null : index);
@@ -195,37 +227,17 @@ export function NavigationStack({
195
227
  return;
196
228
  }
197
229
  const screensToRemove = stackDepth - 1 - selectedRoute.index;
198
- Alert.alert("Pop to Route", `Remove ${screensToRemove} screen${screensToRemove !== 1 ? "s" : ""} above ${selectedRoute.pathname}?`, [{
199
- text: "Cancel",
200
- style: "cancel"
201
- }, {
202
- text: "Pop",
203
- style: "destructive",
204
- onPress: () => {
205
- // Navigate to the selected route, which effectively pops everything above it
206
- navigateToIndex(selectedRoute.index);
207
- }
208
- }]);
230
+ const targetIndex = selectedRoute.index;
231
+ confirmDestructive("Pop to Route", `Remove ${screensToRemove} screen${screensToRemove !== 1 ? "s" : ""} above ${selectedRoute.pathname}?`, "Pop",
232
+ // Navigate to the selected route, which effectively pops everything above it
233
+ () => navigateToIndex(targetIndex));
209
234
  };
210
235
  return /*#__PURE__*/_jsxs(View, {
211
236
  style: [styles.container, style],
212
- children: [/*#__PURE__*/_jsxs(View, {
213
- style: styles.header,
214
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
215
- style: [styles.iconButton, showHelp && styles.iconButtonActive],
216
- onPress: () => setShowHelp(!showHelp),
217
- children: /*#__PURE__*/_jsx(Info, {
218
- size: 16,
219
- color: showHelp ? buoyColors.primary : buoyColors.textSecondary
220
- })
221
- }), /*#__PURE__*/_jsx(InlineCopyButton, {
222
- value: stackDataForCopy,
223
- buttonStyle: styles.iconButton
224
- })]
225
- }), /*#__PURE__*/_jsx(ScrollView, {
237
+ children: [/*#__PURE__*/_jsx(ScrollView, {
226
238
  style: styles.stackScroll,
227
239
  contentContainerStyle: [styles.stackContent, {
228
- paddingBottom: showHelp ? 72 : 56
240
+ paddingBottom: styles.stackContent.padding + insets.bottom
229
241
  }],
230
242
  children: [...stack].reverse().map((item, reverseIndex) => {
231
243
  const actualIndex = stack.length - 1 - reverseIndex;
@@ -294,59 +306,56 @@ export function NavigationStack({
294
306
  data: item.params,
295
307
  showTypeFilter: false
296
308
  })
309
+ }), /*#__PURE__*/_jsxs(View, {
310
+ style: styles.actionsRow,
311
+ children: [/*#__PURE__*/_jsxs(View, {
312
+ style: styles.actionWrapper,
313
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
314
+ style: [styles.actionButton, isAtRoot && styles.actionButtonDisabled],
315
+ onPress: handleGoBack,
316
+ disabled: isAtRoot,
317
+ children: /*#__PURE__*/_jsx(Text, {
318
+ style: [styles.actionButtonText, isAtRoot && styles.actionButtonTextDisabled],
319
+ children: "Back"
320
+ })
321
+ }), /*#__PURE__*/_jsx(Text, {
322
+ style: styles.helpText,
323
+ children: "Go back one screen"
324
+ })]
325
+ }), /*#__PURE__*/_jsxs(View, {
326
+ style: styles.actionWrapper,
327
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
328
+ style: [styles.actionButton, item.isFocused && styles.actionButtonDisabled],
329
+ onPress: handleGo,
330
+ disabled: item.isFocused,
331
+ children: /*#__PURE__*/_jsx(Text, {
332
+ style: [styles.actionButtonText, item.isFocused && styles.actionButtonTextDisabled],
333
+ children: "Go"
334
+ })
335
+ }), /*#__PURE__*/_jsx(Text, {
336
+ style: styles.helpText,
337
+ children: "Navigate to this route"
338
+ })]
339
+ }), /*#__PURE__*/_jsxs(View, {
340
+ style: styles.actionWrapper,
341
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
342
+ style: [styles.actionButton, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonDisabled],
343
+ onPress: handlePopTo,
344
+ disabled: item.isFocused || actualIndex === stackDepth - 1,
345
+ children: /*#__PURE__*/_jsx(Text, {
346
+ style: [styles.actionButtonText, (item.isFocused || actualIndex === stackDepth - 1) && styles.actionButtonTextDisabled],
347
+ children: "Pop To"
348
+ })
349
+ }), /*#__PURE__*/_jsx(Text, {
350
+ style: styles.helpText,
351
+ children: "Remove screens above this"
352
+ })]
353
+ })]
297
354
  })]
298
355
  })]
299
356
  })
300
357
  }, `stack-${actualIndex}-${item.key}`);
301
358
  })
302
- }), /*#__PURE__*/_jsx(View, {
303
- style: styles.actionsContainer,
304
- children: /*#__PURE__*/_jsxs(View, {
305
- style: styles.actionsRow,
306
- children: [/*#__PURE__*/_jsxs(View, {
307
- style: styles.actionWrapper,
308
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
309
- style: [styles.actionButton, isAtRoot && styles.actionButtonDisabled],
310
- onPress: handleGoBack,
311
- disabled: isAtRoot,
312
- children: /*#__PURE__*/_jsx(Text, {
313
- style: [styles.actionButtonText, isAtRoot && styles.actionButtonTextDisabled],
314
- children: "Back"
315
- })
316
- }), showHelp && /*#__PURE__*/_jsx(Text, {
317
- style: styles.helpText,
318
- children: "Go back one screen"
319
- })]
320
- }), /*#__PURE__*/_jsxs(View, {
321
- style: styles.actionWrapper,
322
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
323
- style: [styles.actionButton, selectedRoute?.isFocused && styles.actionButtonDisabled],
324
- onPress: handleGo,
325
- disabled: selectedRoute?.isFocused,
326
- children: /*#__PURE__*/_jsx(Text, {
327
- style: [styles.actionButtonText, selectedRoute?.isFocused && styles.actionButtonTextDisabled],
328
- children: "Go"
329
- })
330
- }), showHelp && /*#__PURE__*/_jsx(Text, {
331
- style: styles.helpText,
332
- children: "Navigate to selected route"
333
- })]
334
- }), /*#__PURE__*/_jsxs(View, {
335
- style: styles.actionWrapper,
336
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
337
- style: [styles.actionButton, (selectedRoute?.isFocused || selectedRoute?.index === stackDepth - 1) && styles.actionButtonDisabled],
338
- onPress: handlePopTo,
339
- disabled: selectedRoute?.isFocused || selectedRoute?.index === stackDepth - 1,
340
- children: /*#__PURE__*/_jsx(Text, {
341
- style: [styles.actionButtonText, (selectedRoute?.isFocused || selectedRoute?.index === stackDepth - 1) && styles.actionButtonTextDisabled],
342
- children: "Pop To"
343
- })
344
- }), showHelp && /*#__PURE__*/_jsx(Text, {
345
- style: styles.helpText,
346
- children: "Remove screens above selected"
347
- })]
348
- })]
349
- })
350
359
  }), /*#__PURE__*/_jsx(ProUpgradeModal, {
351
360
  visible: showUpgradeModal,
352
361
  onClose: () => setShowUpgradeModal(false),
@@ -413,22 +422,6 @@ const styles = StyleSheet.create({
413
422
  fontFamily: "monospace",
414
423
  textAlign: "center"
415
424
  },
416
- header: {
417
- flexDirection: "row",
418
- padding: 8,
419
- gap: 8,
420
- borderBottomWidth: 1,
421
- borderBottomColor: buoyColors.border,
422
- alignItems: "center",
423
- justifyContent: "flex-end"
424
- },
425
- iconButton: {
426
- padding: 6,
427
- borderRadius: 4
428
- },
429
- iconButtonActive: {
430
- backgroundColor: buoyColors.input
431
- },
432
425
  stackScroll: {
433
426
  flex: 1
434
427
  },
@@ -508,22 +501,10 @@ const styles = StyleSheet.create({
508
501
  marginHorizontal: -12,
509
502
  marginBottom: 8
510
503
  },
511
- actionsContainer: {
512
- position: "absolute",
513
- left: 0,
514
- right: 0,
515
- bottom: 0,
516
- borderTopWidth: 1,
517
- borderTopColor: buoyColors.border,
518
- backgroundColor: buoyColors.base,
519
- paddingHorizontal: 8,
520
- paddingTop: 8,
521
- paddingBottom: 8
522
- },
523
504
  actionsRow: {
524
505
  flexDirection: "row",
525
506
  gap: 6,
526
- marginBottom: 6
507
+ marginTop: 12
527
508
  },
528
509
  actionWrapper: {
529
510
  flex: 1
@@ -77,6 +77,9 @@ export function RouteEventsModalWithTabs({
77
77
  const hasExpoRouter = isExpoRouterAvailable();
78
78
  const [activeTab, setActiveTab] = useState("events");
79
79
  const [showUpgradeModal, setShowUpgradeModal] = useState(false);
80
+ // Serialized stack reported up by NavigationStack so the copy button can live
81
+ // in the shared navbar.
82
+ const [stackCopyValue, setStackCopyValue] = useState("");
80
83
 
81
84
  // Check Pro status internally
82
85
  const isPro = useIsPro();
@@ -375,7 +378,8 @@ export function RouteEventsModalWithTabs({
375
378
  }
376
379
  if (activeTab === "stack") {
377
380
  return /*#__PURE__*/_jsx(NavigationStack, {
378
- style: styles.contentWrapper
381
+ style: styles.contentWrapper,
382
+ onCopyValueChange: setStackCopyValue
379
383
  });
380
384
  }
381
385
 
@@ -480,8 +484,11 @@ export function RouteEventsModalWithTabs({
480
484
  activeTab: activeTab,
481
485
  onTabChange: tab => setActiveTab(tab)
482
486
  })
483
- }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
484
- children: activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
487
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
488
+ children: [activeTab === "stack" && /*#__PURE__*/_jsx(ToolbarCopyButton, {
489
+ value: stackCopyValue,
490
+ buttonStyle: styles.iconButton
491
+ }), activeTab === "events" && /*#__PURE__*/_jsxs(_Fragment, {
485
492
  children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
486
493
  value: copyAllEventsData,
487
494
  buttonStyle: styles.iconButton
@@ -504,7 +511,7 @@ export function RouteEventsModalWithTabs({
504
511
  color: buoyColors.error
505
512
  })
506
513
  })]
507
- })
514
+ })]
508
515
  })]
509
516
  })
510
517
  },