@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
@@ -12,9 +12,9 @@
12
12
  */
13
13
 
14
14
  import { useState, useCallback, useMemo, useEffect } from "react";
15
- import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Alert } from "react-native";
15
+ import { View, Text, TextInput, TouchableOpacity, ScrollView, StyleSheet, Alert, Platform } 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, CopyButton, 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,35 @@ import { useRouteSitemap } from "../useRouteSitemap";
22
22
  // Types
23
23
  // ============================================================================
24
24
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+ // On desktop (web) the toolbar has far more room, so scale the action buttons
26
+ // up for easier clicking and a less cramped header.
27
+ const IS_DESKTOP = Platform.OS === "web";
28
+ const ACTION_ICON_SIZE = IS_DESKTOP ? 22 : 16;
29
+
30
+ // Synthetic route representing the app's home/index ("/"). Used by the
31
+ // "Home" toolbar shortcut so navigation works the same as tapping any route.
32
+ const HOME_ROUTE = {
33
+ path: "/",
34
+ name: "index",
35
+ type: "index",
36
+ params: [],
37
+ nodeType: "route",
38
+ contextKey: "/",
39
+ isInternal: false,
40
+ children: [],
41
+ depth: 0
42
+ };
43
+
25
44
  // ============================================================================
26
45
  // Main Component
27
46
  // ============================================================================
28
47
 
29
48
  export function RoutesSitemap({
30
- style
49
+ style,
50
+ injectedRoutes,
51
+ injectedSource,
52
+ injectedLastUpdatedAt,
53
+ onNavigateRoute
31
54
  }) {
32
55
  const [searchQuery, setSearchQuery] = useState("");
33
56
  const [isSearching, setIsSearching] = useState(false);
@@ -49,6 +72,7 @@ export function RoutesSitemap({
49
72
  groups,
50
73
  stats,
51
74
  isLoaded,
75
+ isSupported,
52
76
  filteredRoutes,
53
77
  routes,
54
78
  refresh,
@@ -56,7 +80,10 @@ export function RoutesSitemap({
56
80
  source
57
81
  } = useRouteSitemap({
58
82
  searchQuery,
59
- sortBy: "path"
83
+ sortBy: "path",
84
+ injectedRoutes,
85
+ injectedSource,
86
+ injectedLastUpdatedAt
60
87
  });
61
88
 
62
89
  // Prepare copy data - memoized so it only rebuilds when dependencies change
@@ -143,9 +170,16 @@ export function RoutesSitemap({
143
170
  }
144
171
  }], "plain-text");
145
172
  }, [router]);
146
- const handleNavigate = useCallback(route => {
147
- // Gate behind Pro
148
- if (!isPro) {
173
+ const handleNavigate = useCallback((route, options) => {
174
+ // When a navigation delegate is supplied (dashboard → device), hand off
175
+ // entirely: the delegate owns Pro gating and param resolution.
176
+ if (onNavigateRoute) {
177
+ onNavigateRoute(route);
178
+ return;
179
+ }
180
+
181
+ // Gate behind Pro (the Home shortcut bypasses this — it's always free)
182
+ if (!isPro && !options?.bypassPro) {
149
183
  setShowUpgradeModal(true);
150
184
  return;
151
185
  }
@@ -179,7 +213,12 @@ export function RoutesSitemap({
179
213
  } catch (error) {
180
214
  Alert.alert("Navigation Error", String(error));
181
215
  }
182
- }, [router, promptForParams, isPro]);
216
+ }, [router, promptForParams, isPro, onNavigateRoute]);
217
+ const handleGoHome = useCallback(() => {
218
+ handleNavigate(HOME_ROUTE, {
219
+ bypassPro: true
220
+ });
221
+ }, [handleNavigate]);
183
222
  const handleManualRefresh = useCallback(() => {
184
223
  if (isRefreshing) return;
185
224
  setIsRefreshing(true);
@@ -214,7 +253,7 @@ export function RoutesSitemap({
214
253
  style: styles.loadingContainer,
215
254
  children: /*#__PURE__*/_jsx(Text, {
216
255
  style: styles.loadingText,
217
- children: "Loading routes..."
256
+ 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
257
  })
219
258
  })
220
259
  });
@@ -226,39 +265,54 @@ export function RoutesSitemap({
226
265
  children: [/*#__PURE__*/_jsxs(View, {
227
266
  style: styles.actionsRow,
228
267
  children: [/*#__PURE__*/_jsxs(View, {
229
- style: styles.actionWrapper,
230
- children: [/*#__PURE__*/_jsx(ToolbarCopyButton, {
268
+ style: [styles.actionWrapper, IS_DESKTOP && styles.actionWrapperDesktop],
269
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
270
+ style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop],
271
+ onPress: handleGoHome,
272
+ accessibilityLabel: "Go to home route",
273
+ children: /*#__PURE__*/_jsx(Home, {
274
+ size: ACTION_ICON_SIZE,
275
+ color: buoyColors.textSecondary
276
+ })
277
+ }), /*#__PURE__*/_jsx(Text, {
278
+ style: [styles.actionLabel, IS_DESKTOP && styles.actionLabelDesktop],
279
+ children: "Home"
280
+ })]
281
+ }), /*#__PURE__*/_jsxs(View, {
282
+ style: [styles.actionWrapper, IS_DESKTOP && styles.actionWrapperDesktop],
283
+ children: [/*#__PURE__*/_jsx(CopyButton, {
231
284
  value: copyAllData,
232
- buttonStyle: styles.actionButtonHeader
285
+ size: IS_DESKTOP ? ACTION_ICON_SIZE : 14,
286
+ buttonStyle: IS_DESKTOP ? styles.actionButtonHeaderDesktop : styles.actionButtonHeader
233
287
  }), /*#__PURE__*/_jsx(Text, {
234
- style: styles.actionLabel,
288
+ style: [styles.actionLabel, IS_DESKTOP && styles.actionLabelDesktop],
235
289
  children: "Copy"
236
290
  })]
237
291
  }), /*#__PURE__*/_jsxs(View, {
238
- style: styles.actionWrapper,
292
+ style: [styles.actionWrapper, IS_DESKTOP && styles.actionWrapperDesktop],
239
293
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
240
- style: [styles.iconButton, isRefreshing && styles.refreshButtonDisabled],
294
+ style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop, isRefreshing && styles.refreshButtonDisabled],
241
295
  onPress: handleManualRefresh,
242
296
  disabled: isRefreshing,
243
297
  children: /*#__PURE__*/_jsx(RefreshCw, {
244
- size: 16,
298
+ size: ACTION_ICON_SIZE,
245
299
  color: isRefreshing ? buoyColors.textMuted : buoyColors.textSecondary
246
300
  })
247
301
  }), /*#__PURE__*/_jsx(Text, {
248
- style: styles.actionLabel,
302
+ style: [styles.actionLabel, IS_DESKTOP && styles.actionLabelDesktop],
249
303
  children: "Refresh"
250
304
  })]
251
305
  }), /*#__PURE__*/_jsxs(View, {
252
- style: styles.actionWrapper,
306
+ style: [styles.actionWrapper, IS_DESKTOP && styles.actionWrapperDesktop],
253
307
  children: [/*#__PURE__*/_jsx(TouchableOpacity, {
254
- style: styles.iconButton,
308
+ style: [styles.iconButton, IS_DESKTOP && styles.iconButtonDesktop],
255
309
  onPress: () => setIsSearching(true),
256
310
  children: /*#__PURE__*/_jsx(Search, {
257
- size: 16,
311
+ size: ACTION_ICON_SIZE,
258
312
  color: buoyColors.textSecondary
259
313
  })
260
314
  }), /*#__PURE__*/_jsx(Text, {
261
- style: styles.actionLabel,
315
+ style: [styles.actionLabel, IS_DESKTOP && styles.actionLabelDesktop],
262
316
  children: "Search"
263
317
  })]
264
318
  })]
@@ -404,18 +458,18 @@ function RouteGroupView({
404
458
  children: group.routes.length
405
459
  })
406
460
  })]
407
- }), isExpanded && /*#__PURE__*/_jsxs(View, {
461
+ }), group.description && /*#__PURE__*/_jsx(View, {
462
+ style: styles.groupDescription,
463
+ children: /*#__PURE__*/_jsx(Text, {
464
+ style: styles.groupDescriptionText,
465
+ children: group.description
466
+ })
467
+ }), isExpanded && /*#__PURE__*/_jsx(View, {
408
468
  style: styles.routesList,
409
- children: [group.description && /*#__PURE__*/_jsx(View, {
410
- style: styles.groupDescription,
411
- children: /*#__PURE__*/_jsx(Text, {
412
- style: styles.groupDescriptionText,
413
- children: group.description
414
- })
415
- }), group.routes.map((route, index) => /*#__PURE__*/_jsx(RouteItemView, {
469
+ children: group.routes.map((route, index) => /*#__PURE__*/_jsx(RouteItemView, {
416
470
  route: route,
417
471
  onNavigate: onNavigate
418
- }, `${route.path}-${index}`))]
472
+ }, `${route.path}-${index}`))
419
473
  })]
420
474
  });
421
475
  }
@@ -426,85 +480,85 @@ function RouteItemView({
426
480
  }) {
427
481
  const [isExpanded, setIsExpanded] = useState(false);
428
482
  const hasChildren = route.children.length > 0;
429
- const hasParams = route.params.length > 0;
430
483
  const typeColor = getRouteTypeColor(route.type);
431
484
  const canNavigate = route.type !== "layout" && route.type !== "group";
432
- return /*#__PURE__*/_jsxs(View, {
485
+
486
+ // Leaf routes (no children) have nothing to drill into — expanding would only
487
+ // reveal the Copy/Go actions, so show those inline instead of behind a toggle.
488
+ const isExpandable = hasChildren;
489
+ const showDetails = isExpandable ? isExpanded : true;
490
+ return /*#__PURE__*/_jsx(View, {
433
491
  style: [styles.routeItem, {
434
492
  marginLeft: depth * 12
435
493
  }],
436
- children: [/*#__PURE__*/_jsxs(View, {
494
+ children: /*#__PURE__*/_jsxs(View, {
437
495
  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
496
  children: [/*#__PURE__*/_jsxs(View, {
480
- style: styles.routeButtons,
481
- children: [/*#__PURE__*/_jsx(InlineCopyButton, {
482
- value: route.path,
483
- buttonStyle: styles.actionButton
484
- }), canNavigate && /*#__PURE__*/_jsx(TouchableOpacity, {
485
- style: [styles.actionButton, styles.navigateButton],
486
- onPress: () => onNavigate(route),
487
- children: /*#__PURE__*/_jsx(Text, {
488
- style: styles.navigateButtonText,
489
- children: "Go"
490
- })
491
- })]
492
- }), hasParams && /*#__PURE__*/_jsxs(View, {
493
- style: styles.paramsContainer,
494
- children: [/*#__PURE__*/_jsx(Text, {
495
- style: styles.paramsLabel,
496
- children: "Parameters:"
497
- }), /*#__PURE__*/_jsx(View, {
498
- style: styles.paramsRow,
499
- children: route.params.map(param => /*#__PURE__*/_jsx(View, {
500
- style: styles.paramTag,
497
+ style: styles.routeHeader,
498
+ children: [isExpandable ? /*#__PURE__*/_jsxs(TouchableOpacity, {
499
+ style: styles.routeHeaderLeft,
500
+ onPress: () => setIsExpanded(!isExpanded),
501
+ activeOpacity: 0.7,
502
+ children: [/*#__PURE__*/_jsx(View, {
503
+ style: styles.expandIndicator,
504
+ children: isExpanded ? /*#__PURE__*/_jsx(ChevronDown, {
505
+ size: 14,
506
+ color: buoyColors.textSecondary
507
+ }) : /*#__PURE__*/_jsx(ChevronRight, {
508
+ size: 14,
509
+ color: buoyColors.textSecondary
510
+ })
511
+ }), /*#__PURE__*/_jsx(Text, {
512
+ style: styles.routePath,
513
+ numberOfLines: 3,
514
+ children: route.path
515
+ })]
516
+ }) :
517
+ /*#__PURE__*/
518
+ // Non-toggling header for leaf routes (keeps the chevron column as a
519
+ // spacer so paths stay aligned with expandable siblings).
520
+ _jsxs(View, {
521
+ style: styles.routeHeaderLeft,
522
+ children: [/*#__PURE__*/_jsx(View, {
523
+ style: styles.expandIndicator
524
+ }), /*#__PURE__*/_jsx(Text, {
525
+ style: styles.routePath,
526
+ numberOfLines: 3,
527
+ children: route.path
528
+ })]
529
+ }), /*#__PURE__*/_jsxs(View, {
530
+ style: styles.routeHeaderActions,
531
+ children: [hasChildren && /*#__PURE__*/_jsx(View, {
532
+ style: styles.childCountBadge,
501
533
  children: /*#__PURE__*/_jsx(Text, {
502
- style: styles.paramText,
503
- children: param
534
+ style: styles.childCountText,
535
+ children: route.children.length
504
536
  })
505
- }, param))
537
+ }), /*#__PURE__*/_jsx(View, {
538
+ style: [styles.typeTag, {
539
+ backgroundColor: `${typeColor}15`,
540
+ borderColor: `${typeColor}40`
541
+ }],
542
+ children: /*#__PURE__*/_jsx(Text, {
543
+ style: [styles.typeText, {
544
+ color: typeColor
545
+ }],
546
+ children: route.type.toUpperCase()
547
+ })
548
+ }), /*#__PURE__*/_jsx(InlineCopyButton, {
549
+ value: route.path,
550
+ buttonStyle: styles.iconAction
551
+ }), canNavigate && /*#__PURE__*/_jsx(TouchableOpacity, {
552
+ style: styles.goButton,
553
+ onPress: () => onNavigate(route),
554
+ activeOpacity: 0.7,
555
+ children: /*#__PURE__*/_jsx(Text, {
556
+ style: styles.goButtonText,
557
+ children: "Go"
558
+ })
559
+ })]
506
560
  })]
507
- }), hasChildren && /*#__PURE__*/_jsx(View, {
561
+ }), showDetails && hasChildren && /*#__PURE__*/_jsx(View, {
508
562
  style: styles.childrenContainer,
509
563
  children: route.children.map((child, index) => /*#__PURE__*/_jsx(RouteItemView, {
510
564
  route: child,
@@ -512,7 +566,7 @@ function RouteItemView({
512
566
  onNavigate: onNavigate
513
567
  }, `${child.path}-${index}`))
514
568
  })]
515
- })]
569
+ })
516
570
  });
517
571
  }
518
572
 
@@ -566,7 +620,11 @@ const styles = StyleSheet.create({
566
620
  loadingText: {
567
621
  color: buoyColors.textSecondary,
568
622
  fontSize: 14,
569
- fontFamily: "monospace"
623
+ fontFamily: "monospace",
624
+ textAlign: "center",
625
+ lineHeight: 20,
626
+ paddingHorizontal: 32,
627
+ maxWidth: 420
570
628
  },
571
629
  header: {
572
630
  flexDirection: "column",
@@ -578,7 +636,7 @@ const styles = StyleSheet.create({
578
636
  actionsRow: {
579
637
  flexDirection: "row",
580
638
  alignItems: "center",
581
- justifyContent: "flex-end",
639
+ justifyContent: "space-between",
582
640
  gap: 12,
583
641
  paddingBottom: 4
584
642
  },
@@ -613,22 +671,37 @@ const styles = StyleSheet.create({
613
671
  letterSpacing: 0.5
614
672
  },
615
673
  actionWrapper: {
674
+ flex: 1,
616
675
  alignItems: "center",
617
676
  justifyContent: "center",
618
677
  gap: 4,
619
678
  minWidth: 48
620
679
  },
680
+ actionWrapperDesktop: {
681
+ gap: 8,
682
+ minWidth: 72
683
+ },
621
684
  actionButtonHeader: {
622
685
  padding: 6,
623
686
  borderRadius: 4,
624
687
  backgroundColor: buoyColors.input
625
688
  },
689
+ actionButtonHeaderDesktop: {
690
+ padding: 12,
691
+ borderRadius: 8,
692
+ backgroundColor: buoyColors.input
693
+ },
626
694
  iconButton: {
627
695
  padding: 6,
628
696
  borderRadius: 4,
629
697
  alignItems: "center",
630
698
  justifyContent: "center"
631
699
  },
700
+ iconButtonDesktop: {
701
+ padding: 12,
702
+ borderRadius: 8,
703
+ backgroundColor: buoyColors.input
704
+ },
632
705
  actionLabel: {
633
706
  fontSize: 8,
634
707
  color: buoyColors.textMuted,
@@ -636,6 +709,9 @@ const styles = StyleSheet.create({
636
709
  textTransform: "uppercase",
637
710
  letterSpacing: 0.5
638
711
  },
712
+ actionLabelDesktop: {
713
+ fontSize: 12
714
+ },
639
715
  refreshButtonDisabled: {
640
716
  opacity: 0.5
641
717
  },
@@ -780,15 +856,11 @@ const styles = StyleSheet.create({
780
856
  marginBottom: 6
781
857
  },
782
858
  routeCard: {
783
- flexDirection: "row",
784
- alignItems: "center",
785
- justifyContent: "space-between",
786
- paddingVertical: 10,
787
- paddingHorizontal: 12,
788
859
  backgroundColor: buoyColors.card,
789
860
  borderRadius: 6,
790
861
  borderWidth: 1,
791
862
  borderColor: buoyColors.border,
863
+ overflow: "hidden",
792
864
  shadowColor: "#000",
793
865
  shadowOffset: {
794
866
  width: 0,
@@ -798,8 +870,16 @@ const styles = StyleSheet.create({
798
870
  shadowRadius: 2,
799
871
  elevation: 1
800
872
  },
873
+ routeHeader: {
874
+ flexDirection: "row",
875
+ alignItems: "center",
876
+ justifyContent: "space-between",
877
+ paddingVertical: 7,
878
+ paddingHorizontal: 10,
879
+ gap: 8
880
+ },
801
881
  expandIndicator: {
802
- width: 20,
882
+ width: 18,
803
883
  alignItems: "center"
804
884
  },
805
885
  routeHeaderLeft: {
@@ -848,76 +928,29 @@ const styles = StyleSheet.create({
848
928
  fontWeight: "600",
849
929
  fontFamily: "monospace"
850
930
  },
851
- routeDetails: {
852
- paddingHorizontal: 12,
853
- paddingTop: 8,
854
- paddingBottom: 12,
855
- gap: 12,
856
- borderTopWidth: 1,
857
- borderTopColor: buoyColors.border,
858
- backgroundColor: buoyColors.card,
859
- borderRadius: 6,
931
+ // Compact icon-sized Copy button in the header action cluster.
932
+ iconAction: {
933
+ width: 28,
934
+ height: 24,
935
+ borderRadius: 4,
936
+ backgroundColor: buoyColors.input,
860
937
  borderWidth: 1,
861
938
  borderColor: buoyColors.border,
862
- borderTopLeftRadius: 0,
863
- borderTopRightRadius: 0,
864
- marginTop: -6
865
- },
866
- paramsContainer: {
867
- gap: 6
868
- },
869
- paramsLabel: {
870
- fontSize: 10,
871
- color: buoyColors.textSecondary,
872
- fontFamily: "monospace",
873
- marginBottom: 4
874
- },
875
- paramsRow: {
876
- flexDirection: "row",
877
- flexWrap: "wrap",
878
- gap: 6
939
+ alignItems: "center",
940
+ justifyContent: "center"
879
941
  },
880
- paramTag: {
881
- backgroundColor: "#F59E0B15",
882
- borderColor: "#F59E0B40",
883
- borderWidth: 1,
942
+ // Small "Go" pill in the header action cluster.
943
+ goButton: {
944
+ height: 24,
945
+ paddingHorizontal: 10,
884
946
  borderRadius: 4,
885
- paddingHorizontal: 6,
886
- paddingVertical: 2
887
- },
888
- paramText: {
889
- fontSize: 10,
890
- color: "#F59E0B",
891
- fontFamily: "monospace",
892
- fontWeight: "600"
893
- },
894
- routeButtons: {
895
- flexDirection: "row",
896
- gap: 6,
897
- marginBottom: 12
898
- },
899
- actionButton: {
900
- flexDirection: "row",
901
947
  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: {
948
+ justifyContent: "center",
917
949
  backgroundColor: buoyColors.primary + "15",
950
+ borderWidth: 1,
918
951
  borderColor: buoyColors.primary + "40"
919
952
  },
920
- navigateButtonText: {
953
+ goButtonText: {
921
954
  fontSize: 12,
922
955
  color: buoyColors.primary,
923
956
  fontFamily: "monospace",
@@ -926,6 +959,9 @@ const styles = StyleSheet.create({
926
959
  childrenContainer: {
927
960
  borderLeftWidth: 2,
928
961
  borderLeftColor: buoyColors.border,
929
- marginLeft: 16
962
+ marginLeft: 16,
963
+ marginRight: 8,
964
+ marginTop: 2,
965
+ marginBottom: 8
930
966
  }
931
967
  });
@@ -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");
@@ -38,6 +38,10 @@ export { useRouteObserverReactNavigation } from "./useRouteObserverReactNavigati
38
38
  export { useRouteEvents } from "./hooks/useRouteEvents";
39
39
  export { useRouteSitemap, useRoute, useParentRoutes } from "./useRouteSitemap";
40
40
  export { useNavigationStack } from "./useNavigationStack";
41
+ // =============================================================================
42
+ // EXTERNAL SYNC (Adapter for @buoy-gg/external-sync's useExternalSync)
43
+ // =============================================================================
44
+ export { routeEventsSyncAdapter } from "./sync/routeEventsSyncAdapter";
41
45
 
42
46
  // =============================================================================
43
47
  // TYPES
@@ -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();