@buoy-gg/route-events 3.0.0 → 3.0.2
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 +47 -31
- package/lib/commonjs/components/RoutesSitemap.js +176 -133
- package/lib/commonjs/expoRouterStore.js +17 -0
- package/lib/commonjs/stores/navigationStackStore.js +45 -0
- package/lib/commonjs/sync/routeEventsSyncAdapter.js +125 -3
- package/lib/commonjs/useRouteSitemap.js +28 -7
- package/lib/module/RouteTracker.js +39 -5
- package/lib/module/components/NavigationStack.js +47 -31
- package/lib/module/components/RoutesSitemap.js +177 -134
- package/lib/module/expoRouterStore.js +16 -0
- package/lib/module/stores/navigationStackStore.js +41 -0
- package/lib/module/sync/routeEventsSyncAdapter.js +125 -3
- package/lib/module/useRouteSitemap.js +29 -8
- package/lib/typescript/RouteTracker.d.ts.map +1 -1
- package/lib/typescript/components/NavigationStack.d.ts +18 -1
- package/lib/typescript/components/NavigationStack.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 +2 -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 +52 -1
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/useRouteSitemap.d.ts +17 -0
- package/lib/typescript/useRouteSitemap.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
|
15
15
|
import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Alert } from "react-native";
|
|
16
16
|
import { useSafeRouter } from "@buoy-gg/shared-ui";
|
|
17
|
-
import { Search, ChevronDown, ChevronRight, InlineCopyButton, ToolbarCopyButton, RefreshCw, formatRelativeTime, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
|
|
17
|
+
import { Search, ChevronDown, ChevronRight, Home, InlineCopyButton, ToolbarCopyButton, RefreshCw, formatRelativeTime, ProUpgradeModal, buoyColors } from "@buoy-gg/shared-ui";
|
|
18
18
|
import { useIsPro } from "@buoy-gg/license";
|
|
19
19
|
import { useRouteSitemap } from "../useRouteSitemap";
|
|
20
20
|
|
|
@@ -22,12 +22,30 @@ import { useRouteSitemap } from "../useRouteSitemap";
|
|
|
22
22
|
// Types
|
|
23
23
|
// ============================================================================
|
|
24
24
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
25
|
+
// Synthetic route representing the app's home/index ("/"). Used by the
|
|
26
|
+
// "Home" toolbar shortcut so navigation works the same as tapping any route.
|
|
27
|
+
const HOME_ROUTE = {
|
|
28
|
+
path: "/",
|
|
29
|
+
name: "index",
|
|
30
|
+
type: "index",
|
|
31
|
+
params: [],
|
|
32
|
+
nodeType: "route",
|
|
33
|
+
contextKey: "/",
|
|
34
|
+
isInternal: false,
|
|
35
|
+
children: [],
|
|
36
|
+
depth: 0
|
|
37
|
+
};
|
|
38
|
+
|
|
25
39
|
// ============================================================================
|
|
26
40
|
// Main Component
|
|
27
41
|
// ============================================================================
|
|
28
42
|
|
|
29
43
|
export function RoutesSitemap({
|
|
30
|
-
style
|
|
44
|
+
style,
|
|
45
|
+
injectedRoutes,
|
|
46
|
+
injectedSource,
|
|
47
|
+
injectedLastUpdatedAt,
|
|
48
|
+
onNavigateRoute
|
|
31
49
|
}) {
|
|
32
50
|
const [searchQuery, setSearchQuery] = useState("");
|
|
33
51
|
const [isSearching, setIsSearching] = useState(false);
|
|
@@ -49,6 +67,7 @@ export function RoutesSitemap({
|
|
|
49
67
|
groups,
|
|
50
68
|
stats,
|
|
51
69
|
isLoaded,
|
|
70
|
+
isSupported,
|
|
52
71
|
filteredRoutes,
|
|
53
72
|
routes,
|
|
54
73
|
refresh,
|
|
@@ -56,7 +75,10 @@ export function RoutesSitemap({
|
|
|
56
75
|
source
|
|
57
76
|
} = useRouteSitemap({
|
|
58
77
|
searchQuery,
|
|
59
|
-
sortBy: "path"
|
|
78
|
+
sortBy: "path",
|
|
79
|
+
injectedRoutes,
|
|
80
|
+
injectedSource,
|
|
81
|
+
injectedLastUpdatedAt
|
|
60
82
|
});
|
|
61
83
|
|
|
62
84
|
// Prepare copy data - memoized so it only rebuilds when dependencies change
|
|
@@ -144,6 +166,13 @@ export function RoutesSitemap({
|
|
|
144
166
|
}], "plain-text");
|
|
145
167
|
}, [router]);
|
|
146
168
|
const handleNavigate = useCallback(route => {
|
|
169
|
+
// When a navigation delegate is supplied (dashboard → device), hand off
|
|
170
|
+
// entirely: the delegate owns Pro gating and param resolution.
|
|
171
|
+
if (onNavigateRoute) {
|
|
172
|
+
onNavigateRoute(route);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
147
176
|
// Gate behind Pro
|
|
148
177
|
if (!isPro) {
|
|
149
178
|
setShowUpgradeModal(true);
|
|
@@ -179,7 +208,10 @@ export function RoutesSitemap({
|
|
|
179
208
|
} catch (error) {
|
|
180
209
|
Alert.alert("Navigation Error", String(error));
|
|
181
210
|
}
|
|
182
|
-
}, [router, promptForParams, isPro]);
|
|
211
|
+
}, [router, promptForParams, isPro, onNavigateRoute]);
|
|
212
|
+
const handleGoHome = useCallback(() => {
|
|
213
|
+
handleNavigate(HOME_ROUTE);
|
|
214
|
+
}, [handleNavigate]);
|
|
183
215
|
const handleManualRefresh = useCallback(() => {
|
|
184
216
|
if (isRefreshing) return;
|
|
185
217
|
setIsRefreshing(true);
|
|
@@ -214,7 +246,7 @@ export function RoutesSitemap({
|
|
|
214
246
|
style: styles.loadingContainer,
|
|
215
247
|
children: /*#__PURE__*/_jsx(Text, {
|
|
216
248
|
style: styles.loadingText,
|
|
217
|
-
children: "Loading routes..."
|
|
249
|
+
children: isSupported ? "Loading routes..." : "Route sitemap isn't available on the dashboard — the route tree lives on the device. Use the Events tab to see navigation here."
|
|
218
250
|
})
|
|
219
251
|
})
|
|
220
252
|
});
|
|
@@ -226,6 +258,20 @@ export function RoutesSitemap({
|
|
|
226
258
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
227
259
|
style: styles.actionsRow,
|
|
228
260
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
261
|
+
style: styles.actionWrapper,
|
|
262
|
+
children: [/*#__PURE__*/_jsx(TouchableOpacity, {
|
|
263
|
+
style: styles.iconButton,
|
|
264
|
+
onPress: handleGoHome,
|
|
265
|
+
accessibilityLabel: "Go to home route",
|
|
266
|
+
children: /*#__PURE__*/_jsx(Home, {
|
|
267
|
+
size: 16,
|
|
268
|
+
color: buoyColors.textSecondary
|
|
269
|
+
})
|
|
270
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
271
|
+
style: styles.actionLabel,
|
|
272
|
+
children: "Home"
|
|
273
|
+
})]
|
|
274
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
229
275
|
style: styles.actionWrapper,
|
|
230
276
|
children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
|
|
231
277
|
value: copyAllData,
|
|
@@ -429,82 +475,92 @@ function RouteItemView({
|
|
|
429
475
|
const hasParams = route.params.length > 0;
|
|
430
476
|
const typeColor = getRouteTypeColor(route.type);
|
|
431
477
|
const canNavigate = route.type !== "layout" && route.type !== "group";
|
|
432
|
-
|
|
478
|
+
|
|
479
|
+
// Leaf routes (no children) have nothing to drill into — expanding would only
|
|
480
|
+
// reveal the Copy/Go actions, so show those inline instead of behind a toggle.
|
|
481
|
+
const isExpandable = hasChildren;
|
|
482
|
+
const showDetails = isExpandable ? isExpanded : true;
|
|
483
|
+
return /*#__PURE__*/_jsx(View, {
|
|
433
484
|
style: [styles.routeItem, {
|
|
434
485
|
marginLeft: depth * 12
|
|
435
486
|
}],
|
|
436
|
-
children:
|
|
487
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
437
488
|
style: styles.routeCard,
|
|
438
|
-
children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
439
|
-
style: styles.routeHeaderLeft,
|
|
440
|
-
onPress: () => setIsExpanded(!isExpanded),
|
|
441
|
-
activeOpacity: 0.7,
|
|
442
|
-
children: [/*#__PURE__*/_jsx(View, {
|
|
443
|
-
style: styles.expandIndicator,
|
|
444
|
-
children: isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
|
|
445
|
-
size: 14,
|
|
446
|
-
color: buoyColors.textSecondary
|
|
447
|
-
}) : /*#__PURE__*/_jsx(ChevronRight, {
|
|
448
|
-
size: 14,
|
|
449
|
-
color: buoyColors.textSecondary
|
|
450
|
-
})
|
|
451
|
-
}), /*#__PURE__*/_jsx(Text, {
|
|
452
|
-
style: styles.routePath,
|
|
453
|
-
numberOfLines: 1,
|
|
454
|
-
children: route.path
|
|
455
|
-
})]
|
|
456
|
-
}), /*#__PURE__*/_jsxs(View, {
|
|
457
|
-
style: styles.routeHeaderActions,
|
|
458
|
-
children: [hasChildren && /*#__PURE__*/_jsx(View, {
|
|
459
|
-
style: styles.childCountBadge,
|
|
460
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
461
|
-
style: styles.childCountText,
|
|
462
|
-
children: route.children.length
|
|
463
|
-
})
|
|
464
|
-
}), /*#__PURE__*/_jsx(View, {
|
|
465
|
-
style: [styles.typeTag, {
|
|
466
|
-
backgroundColor: `${typeColor}15`,
|
|
467
|
-
borderColor: `${typeColor}40`
|
|
468
|
-
}],
|
|
469
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
470
|
-
style: [styles.typeText, {
|
|
471
|
-
color: typeColor
|
|
472
|
-
}],
|
|
473
|
-
children: route.type.toUpperCase()
|
|
474
|
-
})
|
|
475
|
-
})]
|
|
476
|
-
})]
|
|
477
|
-
}), isExpanded && /*#__PURE__*/_jsxs(View, {
|
|
478
|
-
style: styles.routeDetails,
|
|
479
489
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
480
|
-
style: styles.
|
|
481
|
-
children: [/*#__PURE__*/
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
490
|
+
style: styles.routeHeader,
|
|
491
|
+
children: [isExpandable ? /*#__PURE__*/_jsxs(TouchableOpacity, {
|
|
492
|
+
style: styles.routeHeaderLeft,
|
|
493
|
+
onPress: () => setIsExpanded(!isExpanded),
|
|
494
|
+
activeOpacity: 0.7,
|
|
495
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
496
|
+
style: styles.expandIndicator,
|
|
497
|
+
children: isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
|
|
498
|
+
size: 14,
|
|
499
|
+
color: buoyColors.textSecondary
|
|
500
|
+
}) : /*#__PURE__*/_jsx(ChevronRight, {
|
|
501
|
+
size: 14,
|
|
502
|
+
color: buoyColors.textSecondary
|
|
503
|
+
})
|
|
504
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
505
|
+
style: styles.routePath,
|
|
506
|
+
numberOfLines: 3,
|
|
507
|
+
children: route.path
|
|
508
|
+
})]
|
|
509
|
+
}) :
|
|
510
|
+
/*#__PURE__*/
|
|
511
|
+
// Non-toggling header for leaf routes (keeps the chevron column as a
|
|
512
|
+
// spacer so paths stay aligned with expandable siblings).
|
|
513
|
+
_jsxs(View, {
|
|
514
|
+
style: styles.routeHeaderLeft,
|
|
515
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
516
|
+
style: styles.expandIndicator
|
|
517
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
518
|
+
style: styles.routePath,
|
|
519
|
+
numberOfLines: 3,
|
|
520
|
+
children: route.path
|
|
521
|
+
})]
|
|
522
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
523
|
+
style: styles.routeHeaderActions,
|
|
524
|
+
children: [hasChildren && /*#__PURE__*/_jsx(View, {
|
|
525
|
+
style: styles.childCountBadge,
|
|
501
526
|
children: /*#__PURE__*/_jsx(Text, {
|
|
502
|
-
style: styles.
|
|
503
|
-
children:
|
|
527
|
+
style: styles.childCountText,
|
|
528
|
+
children: route.children.length
|
|
504
529
|
})
|
|
505
|
-
},
|
|
530
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
531
|
+
style: [styles.typeTag, {
|
|
532
|
+
backgroundColor: `${typeColor}15`,
|
|
533
|
+
borderColor: `${typeColor}40`
|
|
534
|
+
}],
|
|
535
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
536
|
+
style: [styles.typeText, {
|
|
537
|
+
color: typeColor
|
|
538
|
+
}],
|
|
539
|
+
children: route.type.toUpperCase()
|
|
540
|
+
})
|
|
541
|
+
}), /*#__PURE__*/_jsx(InlineCopyButton, {
|
|
542
|
+
value: route.path,
|
|
543
|
+
buttonStyle: styles.iconAction
|
|
544
|
+
}), canNavigate && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
545
|
+
style: styles.goButton,
|
|
546
|
+
onPress: () => onNavigate(route),
|
|
547
|
+
activeOpacity: 0.7,
|
|
548
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
549
|
+
style: styles.goButtonText,
|
|
550
|
+
children: "Go"
|
|
551
|
+
})
|
|
552
|
+
})]
|
|
506
553
|
})]
|
|
507
|
-
}),
|
|
554
|
+
}), showDetails && hasParams && /*#__PURE__*/_jsx(View, {
|
|
555
|
+
style: styles.paramsRow,
|
|
556
|
+
children: route.params.map(param => /*#__PURE__*/_jsx(View, {
|
|
557
|
+
style: styles.paramTag,
|
|
558
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
559
|
+
style: styles.paramText,
|
|
560
|
+
children: param
|
|
561
|
+
})
|
|
562
|
+
}, param))
|
|
563
|
+
}), showDetails && hasChildren && /*#__PURE__*/_jsx(View, {
|
|
508
564
|
style: styles.childrenContainer,
|
|
509
565
|
children: route.children.map((child, index) => /*#__PURE__*/_jsx(RouteItemView, {
|
|
510
566
|
route: child,
|
|
@@ -512,7 +568,7 @@ function RouteItemView({
|
|
|
512
568
|
onNavigate: onNavigate
|
|
513
569
|
}, `${child.path}-${index}`))
|
|
514
570
|
})]
|
|
515
|
-
})
|
|
571
|
+
})
|
|
516
572
|
});
|
|
517
573
|
}
|
|
518
574
|
|
|
@@ -566,7 +622,11 @@ const styles = StyleSheet.create({
|
|
|
566
622
|
loadingText: {
|
|
567
623
|
color: buoyColors.textSecondary,
|
|
568
624
|
fontSize: 14,
|
|
569
|
-
fontFamily: "monospace"
|
|
625
|
+
fontFamily: "monospace",
|
|
626
|
+
textAlign: "center",
|
|
627
|
+
lineHeight: 20,
|
|
628
|
+
paddingHorizontal: 32,
|
|
629
|
+
maxWidth: 420
|
|
570
630
|
},
|
|
571
631
|
header: {
|
|
572
632
|
flexDirection: "column",
|
|
@@ -578,7 +638,7 @@ const styles = StyleSheet.create({
|
|
|
578
638
|
actionsRow: {
|
|
579
639
|
flexDirection: "row",
|
|
580
640
|
alignItems: "center",
|
|
581
|
-
justifyContent: "
|
|
641
|
+
justifyContent: "space-between",
|
|
582
642
|
gap: 12,
|
|
583
643
|
paddingBottom: 4
|
|
584
644
|
},
|
|
@@ -613,6 +673,7 @@ const styles = StyleSheet.create({
|
|
|
613
673
|
letterSpacing: 0.5
|
|
614
674
|
},
|
|
615
675
|
actionWrapper: {
|
|
676
|
+
flex: 1,
|
|
616
677
|
alignItems: "center",
|
|
617
678
|
justifyContent: "center",
|
|
618
679
|
gap: 4,
|
|
@@ -780,15 +841,11 @@ const styles = StyleSheet.create({
|
|
|
780
841
|
marginBottom: 6
|
|
781
842
|
},
|
|
782
843
|
routeCard: {
|
|
783
|
-
flexDirection: "row",
|
|
784
|
-
alignItems: "center",
|
|
785
|
-
justifyContent: "space-between",
|
|
786
|
-
paddingVertical: 10,
|
|
787
|
-
paddingHorizontal: 12,
|
|
788
844
|
backgroundColor: buoyColors.card,
|
|
789
845
|
borderRadius: 6,
|
|
790
846
|
borderWidth: 1,
|
|
791
847
|
borderColor: buoyColors.border,
|
|
848
|
+
overflow: "hidden",
|
|
792
849
|
shadowColor: "#000",
|
|
793
850
|
shadowOffset: {
|
|
794
851
|
width: 0,
|
|
@@ -798,8 +855,16 @@ const styles = StyleSheet.create({
|
|
|
798
855
|
shadowRadius: 2,
|
|
799
856
|
elevation: 1
|
|
800
857
|
},
|
|
858
|
+
routeHeader: {
|
|
859
|
+
flexDirection: "row",
|
|
860
|
+
alignItems: "center",
|
|
861
|
+
justifyContent: "space-between",
|
|
862
|
+
paddingVertical: 7,
|
|
863
|
+
paddingHorizontal: 10,
|
|
864
|
+
gap: 8
|
|
865
|
+
},
|
|
801
866
|
expandIndicator: {
|
|
802
|
-
width:
|
|
867
|
+
width: 18,
|
|
803
868
|
alignItems: "center"
|
|
804
869
|
},
|
|
805
870
|
routeHeaderLeft: {
|
|
@@ -848,34 +913,41 @@ const styles = StyleSheet.create({
|
|
|
848
913
|
fontWeight: "600",
|
|
849
914
|
fontFamily: "monospace"
|
|
850
915
|
},
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
borderTopColor: buoyColors.border,
|
|
858
|
-
backgroundColor: buoyColors.card,
|
|
859
|
-
borderRadius: 6,
|
|
916
|
+
// Compact icon-sized Copy button in the header action cluster.
|
|
917
|
+
iconAction: {
|
|
918
|
+
width: 28,
|
|
919
|
+
height: 24,
|
|
920
|
+
borderRadius: 4,
|
|
921
|
+
backgroundColor: buoyColors.input,
|
|
860
922
|
borderWidth: 1,
|
|
861
923
|
borderColor: buoyColors.border,
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
marginTop: -6
|
|
924
|
+
alignItems: "center",
|
|
925
|
+
justifyContent: "center"
|
|
865
926
|
},
|
|
866
|
-
|
|
867
|
-
|
|
927
|
+
// Small "Go" pill in the header action cluster.
|
|
928
|
+
goButton: {
|
|
929
|
+
height: 24,
|
|
930
|
+
paddingHorizontal: 10,
|
|
931
|
+
borderRadius: 4,
|
|
932
|
+
alignItems: "center",
|
|
933
|
+
justifyContent: "center",
|
|
934
|
+
backgroundColor: buoyColors.primary + "15",
|
|
935
|
+
borderWidth: 1,
|
|
936
|
+
borderColor: buoyColors.primary + "40"
|
|
868
937
|
},
|
|
869
|
-
|
|
870
|
-
fontSize:
|
|
871
|
-
color: buoyColors.
|
|
938
|
+
goButtonText: {
|
|
939
|
+
fontSize: 12,
|
|
940
|
+
color: buoyColors.primary,
|
|
872
941
|
fontFamily: "monospace",
|
|
873
|
-
|
|
942
|
+
fontWeight: "600"
|
|
874
943
|
},
|
|
875
944
|
paramsRow: {
|
|
876
945
|
flexDirection: "row",
|
|
877
946
|
flexWrap: "wrap",
|
|
878
|
-
gap: 6
|
|
947
|
+
gap: 6,
|
|
948
|
+
paddingHorizontal: 10,
|
|
949
|
+
paddingTop: 2,
|
|
950
|
+
paddingBottom: 8
|
|
879
951
|
},
|
|
880
952
|
paramTag: {
|
|
881
953
|
backgroundColor: "#F59E0B15",
|
|
@@ -891,41 +963,12 @@ const styles = StyleSheet.create({
|
|
|
891
963
|
fontFamily: "monospace",
|
|
892
964
|
fontWeight: "600"
|
|
893
965
|
},
|
|
894
|
-
routeButtons: {
|
|
895
|
-
flexDirection: "row",
|
|
896
|
-
gap: 6,
|
|
897
|
-
marginBottom: 12
|
|
898
|
-
},
|
|
899
|
-
actionButton: {
|
|
900
|
-
flexDirection: "row",
|
|
901
|
-
alignItems: "center",
|
|
902
|
-
gap: 4,
|
|
903
|
-
backgroundColor: buoyColors.input,
|
|
904
|
-
borderRadius: 4,
|
|
905
|
-
paddingHorizontal: 12,
|
|
906
|
-
paddingVertical: 6,
|
|
907
|
-
borderWidth: 1,
|
|
908
|
-
borderColor: buoyColors.border
|
|
909
|
-
},
|
|
910
|
-
actionButtonText: {
|
|
911
|
-
fontSize: 12,
|
|
912
|
-
color: buoyColors.textSecondary,
|
|
913
|
-
fontFamily: "monospace",
|
|
914
|
-
fontWeight: "600"
|
|
915
|
-
},
|
|
916
|
-
navigateButton: {
|
|
917
|
-
backgroundColor: buoyColors.primary + "15",
|
|
918
|
-
borderColor: buoyColors.primary + "40"
|
|
919
|
-
},
|
|
920
|
-
navigateButtonText: {
|
|
921
|
-
fontSize: 12,
|
|
922
|
-
color: buoyColors.primary,
|
|
923
|
-
fontFamily: "monospace",
|
|
924
|
-
fontWeight: "600"
|
|
925
|
-
},
|
|
926
966
|
childrenContainer: {
|
|
927
967
|
borderLeftWidth: 2,
|
|
928
968
|
borderLeftColor: buoyColors.border,
|
|
929
|
-
marginLeft: 16
|
|
969
|
+
marginLeft: 16,
|
|
970
|
+
marginRight: 8,
|
|
971
|
+
marginTop: 2,
|
|
972
|
+
marginBottom: 8
|
|
930
973
|
}
|
|
931
974
|
});
|
|
@@ -24,10 +24,26 @@ function logOnce(message, error) {
|
|
|
24
24
|
* The storeRef gets populated when Expo Router's useStore() hook runs.
|
|
25
25
|
* So we cache the store reference but its property values update over time.
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* Whether the expo-router store can be loaded in this runtime. The store is
|
|
29
|
+
* pulled in via CommonJS `require`, which only exists under Metro/Node. On the
|
|
30
|
+
* web (e.g. the desktop dashboard rendering these RN tools via react-native-web)
|
|
31
|
+
* `require` is undefined, the route tree lives on the device — not here — and
|
|
32
|
+
* there is nothing to load. Callers use this to skip polling/logging.
|
|
33
|
+
*/
|
|
34
|
+
export function isExpoRouterStoreSupported() {
|
|
35
|
+
return typeof require !== "undefined";
|
|
36
|
+
}
|
|
27
37
|
export function getExpoRouterStore() {
|
|
28
38
|
if (cachedStore) {
|
|
29
39
|
return cachedStore;
|
|
30
40
|
}
|
|
41
|
+
|
|
42
|
+
// No `require` (web): bail quietly rather than throwing ReferenceError and
|
|
43
|
+
// logging a misleading "install expo-router" message on the dashboard.
|
|
44
|
+
if (!isExpoRouterStoreSupported()) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
31
47
|
const loadFromBuild = () => {
|
|
32
48
|
try {
|
|
33
49
|
const module = require("expo-router/build/global-state/router-store");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Navigation Stack Store
|
|
5
|
+
*
|
|
6
|
+
* Holds the latest serializable navigation stack plus references to the live
|
|
7
|
+
* navigation action functions. The stack is captured inside the navigation tree
|
|
8
|
+
* by <RouteTracker /> (which runs useNavigationStack), and read by the
|
|
9
|
+
* route-events sync adapter so the dashboard can render the Stack tab and drive
|
|
10
|
+
* navigation on the device remotely.
|
|
11
|
+
*
|
|
12
|
+
* The stack itself (StackDisplayItem[]) is JSON-serializable and synced to the
|
|
13
|
+
* dashboard. The action functions are NOT serialized — they stay on the device
|
|
14
|
+
* and are invoked when the dashboard sends a remote action.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class NavigationStackStore {
|
|
18
|
+
stack = [];
|
|
19
|
+
actions = null;
|
|
20
|
+
listeners = new Set();
|
|
21
|
+
getStack() {
|
|
22
|
+
return this.stack;
|
|
23
|
+
}
|
|
24
|
+
setStack(stack) {
|
|
25
|
+
this.stack = stack;
|
|
26
|
+
this.listeners.forEach(listener => listener());
|
|
27
|
+
}
|
|
28
|
+
getActions() {
|
|
29
|
+
return this.actions;
|
|
30
|
+
}
|
|
31
|
+
setActions(actions) {
|
|
32
|
+
this.actions = actions;
|
|
33
|
+
}
|
|
34
|
+
subscribe(listener) {
|
|
35
|
+
this.listeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
this.listeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export const navigationStackStore = new NavigationStackStore();
|
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
import { getSafeRouter } from "@buoy-gg/shared-ui";
|
|
3
4
|
import { routeEventStore } from "../stores/routeEventStore";
|
|
5
|
+
import { navigationStackStore } from "../stores/navigationStackStore";
|
|
6
|
+
import { RouteParser } from "../RouteParser";
|
|
7
|
+
import { loadRouteNode, getRouteNodeMetadata } from "../expoRouterStore";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serializable snapshot of the device's route tree, sent to the dashboard so
|
|
11
|
+
* the Routes tab can render a sitemap (the raw expo-router RouteNode itself is
|
|
12
|
+
* not JSON-serializable — it holds components/functions — so we send the parsed
|
|
13
|
+
* RouteInfo[] instead).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Payload shape for the route-events tool (adapter version 3). Consumers must
|
|
18
|
+
* tolerate older shapes: v1 sends a bare RouteChangeEvent[]; v2 omits `stack`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
function getSitemapSnapshot() {
|
|
22
|
+
const metadata = getRouteNodeMetadata();
|
|
23
|
+
return {
|
|
24
|
+
routes: RouteParser.parseRouteTree(loadRouteNode()),
|
|
25
|
+
source: metadata.source,
|
|
26
|
+
lastUpdatedAt: metadata.lastLoadedAt
|
|
27
|
+
};
|
|
28
|
+
}
|
|
4
29
|
|
|
5
30
|
/**
|
|
6
31
|
* Sync adapter for the route-events tool, consumed by @buoy-gg/external-sync's
|
|
@@ -10,14 +35,111 @@ import { routeEventStore } from "../stores/routeEventStore";
|
|
|
10
35
|
* Subscribing attaches the routeObserver listener, so route changes are only
|
|
11
36
|
* recorded while a dashboard is watching. (Route events still require a
|
|
12
37
|
* <RouteTracker /> inside the navigation tree to be emitted at all.)
|
|
38
|
+
*
|
|
39
|
+
* The snapshot carries both the route-change events AND the parsed route
|
|
40
|
+
* sitemap, so the dashboard — which has no local expo-router store — can render
|
|
41
|
+
* the route tree. The `navigate` action lets the dashboard drive navigation on
|
|
42
|
+
* the device.
|
|
13
43
|
*/
|
|
14
44
|
export const routeEventsSyncAdapter = {
|
|
15
|
-
version:
|
|
16
|
-
getSnapshot: () =>
|
|
17
|
-
|
|
45
|
+
version: 3,
|
|
46
|
+
getSnapshot: () => ({
|
|
47
|
+
events: routeEventStore.getEvents(),
|
|
48
|
+
sitemap: getSitemapSnapshot(),
|
|
49
|
+
stack: navigationStackStore.getStack()
|
|
50
|
+
}),
|
|
51
|
+
subscribe: onChange => {
|
|
52
|
+
const unsubscribeEvents = routeEventStore.subscribeToEvents(onChange);
|
|
53
|
+
const unsubscribeStack = navigationStackStore.subscribe(onChange);
|
|
54
|
+
|
|
55
|
+
// The expo-router route tree can still be populating when a dashboard
|
|
56
|
+
// starts watching. Poll briefly until it's available and push a snapshot
|
|
57
|
+
// the moment it loads, so the Routes tab fills in without needing the user
|
|
58
|
+
// to navigate first. Stops as soon as the tree is found.
|
|
59
|
+
let pollTimer = null;
|
|
60
|
+
if (!loadRouteNode()) {
|
|
61
|
+
let retries = 0;
|
|
62
|
+
const maxRetries = 100; // ~10s
|
|
63
|
+
const poll = () => {
|
|
64
|
+
retries += 1;
|
|
65
|
+
if (loadRouteNode()) {
|
|
66
|
+
pollTimer = null;
|
|
67
|
+
onChange();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
pollTimer = retries < maxRetries ? setTimeout(poll, 100) : null;
|
|
71
|
+
};
|
|
72
|
+
pollTimer = setTimeout(poll, 100);
|
|
73
|
+
}
|
|
74
|
+
return () => {
|
|
75
|
+
unsubscribeEvents();
|
|
76
|
+
unsubscribeStack();
|
|
77
|
+
if (pollTimer) clearTimeout(pollTimer);
|
|
78
|
+
};
|
|
79
|
+
},
|
|
18
80
|
actions: {
|
|
19
81
|
clearEvents: () => {
|
|
20
82
|
routeEventStore.clearEvents();
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* Navigate the device to a concrete path. The dashboard resolves any
|
|
86
|
+
* dynamic params into a concrete path before invoking this.
|
|
87
|
+
*/
|
|
88
|
+
navigate: params => {
|
|
89
|
+
const path = params?.path;
|
|
90
|
+
if (!path) {
|
|
91
|
+
throw new Error("navigate requires a 'path' param");
|
|
92
|
+
}
|
|
93
|
+
const router = getSafeRouter();
|
|
94
|
+
if (!router) {
|
|
95
|
+
throw new Error("expo-router is not available on this device");
|
|
96
|
+
}
|
|
97
|
+
router.navigate(path);
|
|
98
|
+
return {
|
|
99
|
+
navigated: path
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
// ── Stack actions: delegate to the live navigation actions captured by
|
|
103
|
+
// <RouteTracker /> (they hold the React Navigation container ref). ──
|
|
104
|
+
stackNavigateToIndex: params => {
|
|
105
|
+
const index = params?.index;
|
|
106
|
+
if (typeof index !== "number") {
|
|
107
|
+
throw new Error("stackNavigateToIndex requires a numeric 'index'");
|
|
108
|
+
}
|
|
109
|
+
const actions = navigationStackStore.getActions();
|
|
110
|
+
if (!actions) throw new Error("navigation stack is not available");
|
|
111
|
+
actions.navigateToIndex(index);
|
|
112
|
+
return {
|
|
113
|
+
navigatedToIndex: index
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
stackPopToIndex: params => {
|
|
117
|
+
const index = params?.index;
|
|
118
|
+
if (typeof index !== "number") {
|
|
119
|
+
throw new Error("stackPopToIndex requires a numeric 'index'");
|
|
120
|
+
}
|
|
121
|
+
const actions = navigationStackStore.getActions();
|
|
122
|
+
if (!actions) throw new Error("navigation stack is not available");
|
|
123
|
+
actions.popToIndex(index);
|
|
124
|
+
return {
|
|
125
|
+
poppedToIndex: index
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
stackGoBack: () => {
|
|
129
|
+
const actions = navigationStackStore.getActions();
|
|
130
|
+
if (!actions) throw new Error("navigation stack is not available");
|
|
131
|
+
actions.goBack();
|
|
132
|
+
return {
|
|
133
|
+
wentBack: true
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
stackPopToTop: () => {
|
|
137
|
+
const actions = navigationStackStore.getActions();
|
|
138
|
+
if (!actions) throw new Error("navigation stack is not available");
|
|
139
|
+
actions.popToTop();
|
|
140
|
+
return {
|
|
141
|
+
poppedToTop: true
|
|
142
|
+
};
|
|
21
143
|
}
|
|
22
144
|
}
|
|
23
145
|
};
|