@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.
- package/lib/commonjs/RouteTracker.js +38 -4
- package/lib/commonjs/components/NavigationStack.js +105 -124
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +11 -4
- package/lib/commonjs/components/RoutesSitemap.js +205 -169
- package/lib/commonjs/expoRouterStore.js +17 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/stores/navigationStackStore.js +45 -0
- package/lib/commonjs/sync/routeEventsSyncAdapter.js +148 -0
- package/lib/commonjs/useRouteSitemap.js +28 -7
- package/lib/module/RouteTracker.js +39 -5
- package/lib/module/components/NavigationStack.js +106 -125
- package/lib/module/components/RouteEventsModalWithTabs.js +11 -4
- package/lib/module/components/RoutesSitemap.js +207 -171
- package/lib/module/expoRouterStore.js +16 -0
- package/lib/module/index.js +4 -0
- package/lib/module/stores/navigationStackStore.js +41 -0
- package/lib/module/sync/routeEventsSyncAdapter.js +145 -0
- package/lib/module/useRouteSitemap.js +29 -8
- package/lib/typescript/RouteTracker.d.ts.map +1 -1
- package/lib/typescript/components/NavigationStack.d.ts +24 -1
- package/lib/typescript/components/NavigationStack.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/components/RoutesSitemap.d.ts +19 -1
- package/lib/typescript/components/RoutesSitemap.d.ts.map +1 -1
- package/lib/typescript/expoRouterStore.d.ts +8 -0
- package/lib/typescript/expoRouterStore.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/stores/navigationStackStore.d.ts +33 -0
- package/lib/typescript/stores/navigationStackStore.d.ts.map +1 -0
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts +69 -0
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/useRouteSitemap.d.ts +17 -0
- package/lib/typescript/useRouteSitemap.d.ts.map +1 -1
- 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
|
-
//
|
|
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 {
|
|
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
|
-
|
|
59
|
-
|
|
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,
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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__*/
|
|
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:
|
|
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
|
-
|
|
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__*/
|
|
484
|
-
children: activeTab === "
|
|
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
|
},
|