@buoy-gg/route-events 7.0.1 → 7.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.
Files changed (27) hide show
  1. package/lib/commonjs/components/RouteEventDetail.js +90 -0
  2. package/lib/commonjs/components/RouteEventItemCompact.js +3 -19
  3. package/lib/commonjs/components/RouteEventsModalWithTabs.js +49 -3
  4. package/lib/commonjs/components/RouteEventsTimeline.js +10 -9
  5. package/lib/commonjs/index.js +7 -0
  6. package/lib/commonjs/utils/routeTemplate.js +28 -0
  7. package/lib/module/components/RouteEventDetail.js +86 -0
  8. package/lib/module/components/RouteEventItemCompact.js +1 -17
  9. package/lib/module/components/RouteEventsModalWithTabs.js +49 -3
  10. package/lib/module/components/RouteEventsTimeline.js +11 -10
  11. package/lib/module/index.js +1 -0
  12. package/lib/module/utils/routeTemplate.js +24 -0
  13. package/lib/typescript/components/RouteEventDetail.d.ts +18 -0
  14. package/lib/typescript/components/RouteEventDetail.d.ts.map +1 -0
  15. package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -1
  16. package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
  17. package/lib/typescript/components/RouteEventsTimeline.d.ts +4 -2
  18. package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -1
  19. package/lib/typescript/index.d.ts +2 -0
  20. package/lib/typescript/index.d.ts.map +1 -1
  21. package/lib/typescript/utils/routeTemplate.d.ts +8 -0
  22. package/lib/typescript/utils/routeTemplate.d.ts.map +1 -0
  23. package/package.json +5 -5
  24. package/lib/commonjs/components/RouteEventDetailContent.js +0 -499
  25. package/lib/module/components/RouteEventDetailContent.js +0 -494
  26. package/lib/typescript/components/RouteEventDetailContent.d.ts +0 -21
  27. package/lib/typescript/components/RouteEventDetailContent.d.ts.map +0 -1
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.RouteEventDetail = RouteEventDetail;
7
+ var _reactNative = require("react-native");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
+ var _RouteEventExpandedContent = require("./RouteEventExpandedContent");
10
+ var _routeTemplate = require("../utils/routeTemplate");
11
+ var _jsxRuntime = require("react/jsx-runtime");
12
+ /**
13
+ * RouteEventDetail - Full-screen detail view for a single route event.
14
+ *
15
+ * Mirrors how the Events tool shows a detail page for every other event
16
+ * source: the readable route sections (reusing RouteEventExpandedContent)
17
+ * plus a prominent "Go to route" action. The back header and toolbar copy
18
+ * button are owned by the surrounding modal.
19
+ */
20
+
21
+ function RouteEventDetail({
22
+ event,
23
+ visitNumber = 1,
24
+ onNavigate
25
+ }) {
26
+ const routeTemplate = (0, _routeTemplate.getRouteTemplate)(event.pathname, event.segments);
27
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
28
+ style: styles.container,
29
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
30
+ style: styles.scroll,
31
+ contentContainerStyle: styles.scrollContent,
32
+ showsVerticalScrollIndicator: true,
33
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventExpandedContent.RouteEventExpandedContent, {
34
+ event: event,
35
+ visitNumber: visitNumber,
36
+ routeTemplate: routeTemplate
37
+ })
38
+ }), onNavigate && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
39
+ style: styles.footer,
40
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
41
+ style: styles.goButton,
42
+ onPress: () => onNavigate(event.pathname),
43
+ activeOpacity: 0.8,
44
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Navigation, {
45
+ size: 16,
46
+ color: "#FFFFFF"
47
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
48
+ style: styles.goButtonText,
49
+ children: "Go to route"
50
+ })]
51
+ })
52
+ })]
53
+ });
54
+ }
55
+ const styles = _reactNative.StyleSheet.create({
56
+ container: {
57
+ flex: 1,
58
+ backgroundColor: _sharedUi.buoyColors.base
59
+ },
60
+ scroll: {
61
+ flex: 1
62
+ },
63
+ scrollContent: {
64
+ padding: 14
65
+ },
66
+ footer: {
67
+ paddingHorizontal: 16,
68
+ paddingVertical: 12,
69
+ backgroundColor: _sharedUi.buoyColors.base,
70
+ borderTopWidth: 1,
71
+ borderTopColor: _sharedUi.buoyColors.border
72
+ },
73
+ goButton: {
74
+ flexDirection: "row",
75
+ alignItems: "center",
76
+ justifyContent: "center",
77
+ gap: 8,
78
+ backgroundColor: _sharedUi.buoyColors.primary,
79
+ paddingVertical: 12,
80
+ borderRadius: 8
81
+ },
82
+ goButtonText: {
83
+ fontSize: 13,
84
+ fontWeight: "600",
85
+ color: "#FFFFFF",
86
+ fontFamily: "monospace",
87
+ textTransform: "uppercase",
88
+ letterSpacing: 0.5
89
+ }
90
+ });
@@ -8,6 +8,7 @@ var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _RouteEventExpandedContent = require("./RouteEventExpandedContent");
11
+ var _routeTemplate = require("../utils/routeTemplate");
11
12
  var _jsxRuntime = require("react/jsx-runtime");
12
13
  /**
13
14
  * RouteEventItemCompact - Compact list item for route events
@@ -17,29 +18,12 @@ var _jsxRuntime = require("react/jsx-runtime");
17
18
 
18
19
  // Route type for color coding
19
20
 
20
- // Infer route template from pathname and segments
21
- function getRouteTemplate(pathname, segments) {
22
- if (!segments || segments.length === 0) return null;
23
- const templateParts = segments.map(segment => {
24
- // Check if this segment appears to be a dynamic parameter
25
- if (/^\d+$/.test(segment)) {
26
- return "[id]";
27
- }
28
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
29
- return "[id]";
30
- }
31
- return segment;
32
- });
33
- const template = "/" + templateParts.join("/");
34
- return template !== pathname ? template : null;
35
- }
36
-
37
21
  // Get route type for color coding
38
22
  function getRouteType(event) {
39
23
  if (event.pathname === "/") {
40
24
  return "home";
41
25
  }
42
- if (getRouteTemplate(event.pathname, event.segments)) {
26
+ if ((0, _routeTemplate.getRouteTemplate)(event.pathname, event.segments)) {
43
27
  return "dynamic";
44
28
  }
45
29
  if (event.params && Object.keys(event.params).length > 0) {
@@ -127,7 +111,7 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
127
111
  index
128
112
  }) {
129
113
  const routeType = (0, _react.useMemo)(() => getRouteType(event), [event]);
130
- const routeTemplate = (0, _react.useMemo)(() => getRouteTemplate(event.pathname, event.segments), [event.pathname, event.segments]);
114
+ const routeTemplate = (0, _react.useMemo)(() => (0, _routeTemplate.getRouteTemplate)(event.pathname, event.segments), [event.pathname, event.segments]);
131
115
  const statusColor = (0, _react.useMemo)(() => getRouteTypeColor(routeType), [routeType]);
132
116
  const statusLabel = (0, _react.useMemo)(() => getStatusLabel(routeType), [routeType]);
133
117
  const statusSublabel = (0, _react.useMemo)(() => getStatusSublabel(event), [event]);
@@ -10,6 +10,7 @@ var _safeReactNavigation = require("../utils/safeReactNavigation");
10
10
  var _sharedUi = require("@buoy-gg/shared-ui");
11
11
  var _license = require("@buoy-gg/license");
12
12
  var _useRouteEvents = require("../hooks/useRouteEvents");
13
+ var _RouteEventDetail = require("./RouteEventDetail");
13
14
  var _RouteFilterViewV = require("./RouteFilterViewV2");
14
15
  var _RoutesSitemap = require("./RoutesSitemap");
15
16
  var _ReactNavigationRoutes = require("./ReactNavigationRoutes");
@@ -117,6 +118,10 @@ function RouteEventsModalWithTabs({
117
118
  // isListening = enabled and store is listening
118
119
  const isListening = isListeningEnabled && isStoreListening;
119
120
  const [showFilters, setShowFilters] = (0, _react.useState)(false);
121
+ // Selected event drives the full-screen detail view (parity with the Events
122
+ // tool). null = list view. visitNumber feeds the "Visited N times" section.
123
+ const [selectedEvent, setSelectedEvent] = (0, _react.useState)(null);
124
+ const [selectedVisitNumber, setSelectedVisitNumber] = (0, _react.useState)(1);
120
125
  const [searchQuery, setSearchQuery] = (0, _react.useState)("");
121
126
  const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set(["/_sitemap", "/api", "/__dev"]));
122
127
  const lastEventRef = (0, _react.useRef)(null);
@@ -283,6 +288,18 @@ function RouteEventsModalWithTabs({
283
288
  const handleToggleFilters = (0, _react.useCallback)(() => {
284
289
  setShowFilters(!showFilters);
285
290
  }, [showFilters]);
291
+ const handleSelectEvent = (0, _react.useCallback)((event, visitNumber) => {
292
+ setSelectedEvent(event);
293
+ setSelectedVisitNumber(visitNumber);
294
+ }, []);
295
+ const handleBackFromDetail = (0, _react.useCallback)(() => {
296
+ setSelectedEvent(null);
297
+ }, []);
298
+ const handleTabChange = (0, _react.useCallback)(tab => {
299
+ // Leaving the detail view whenever the tab changes.
300
+ setSelectedEvent(null);
301
+ setActiveTab(tab);
302
+ }, []);
286
303
 
287
304
  // Get all unique pathnames from events
288
305
  const allEventPathnames = (0, _react.useMemo)(() => {
@@ -375,6 +392,14 @@ function RouteEventsModalWithTabs({
375
392
  // No footer needed since we're showing a timeline
376
393
  const footerNode = null;
377
394
  const renderContent = () => {
395
+ // Detail view takes priority — a selected event replaces the list.
396
+ if (selectedEvent) {
397
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventDetail.RouteEventDetail, {
398
+ event: selectedEvent,
399
+ visitNumber: selectedVisitNumber,
400
+ onNavigate: handleNavigate
401
+ });
402
+ }
378
403
  if (activeTab === "routes") {
379
404
  return hasExpoRouter ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_RoutesSitemap.RoutesSitemap, {
380
405
  style: styles.contentWrapper
@@ -452,7 +477,8 @@ function RouteEventsModalWithTabs({
452
477
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventsTimeline.RouteEventsTimeline, {
453
478
  events: visibleEvents,
454
479
  visitCounts: visitCounts,
455
- onNavigate: handleNavigate
480
+ onNavigate: handleNavigate,
481
+ onSelectEvent: handleSelectEvent
456
482
  })]
457
483
  });
458
484
  };
@@ -464,7 +490,27 @@ function RouteEventsModalWithTabs({
464
490
  persistenceKey: persistenceKey,
465
491
  header: {
466
492
  showToggleButton: true,
467
- customContent: showFilters ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
493
+ customContent: selectedEvent ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
494
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
495
+ onBack: handleBackFromDetail
496
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
497
+ title: selectedEvent.pathname
498
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
499
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
500
+ value: {
501
+ pathname: selectedEvent.pathname,
502
+ previousPathname: selectedEvent.previousPathname,
503
+ params: selectedEvent.params,
504
+ segments: selectedEvent.segments,
505
+ timestamp: selectedEvent.timestamp,
506
+ timestampISO: new Date(selectedEvent.timestamp).toISOString(),
507
+ timeSincePrevious: selectedEvent.timeSincePrevious,
508
+ visitNumber: selectedVisitNumber
509
+ },
510
+ buttonStyle: styles.iconButton
511
+ })
512
+ })]
513
+ }) : showFilters ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
468
514
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
469
515
  onBack: () => setShowFilters(false)
470
516
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
@@ -488,7 +534,7 @@ function RouteEventsModalWithTabs({
488
534
  label: "Stack"
489
535
  }],
490
536
  activeTab: activeTab,
491
- onTabChange: tab => setActiveTab(tab)
537
+ onTabChange: tab => handleTabChange(tab)
492
538
  })
493
539
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
494
540
  children: [activeTab === "stack" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
@@ -15,18 +15,21 @@ var _jsxRuntime = require("react/jsx-runtime");
15
15
  * Shows events in the order they happened (most recent first),
16
16
  * providing a clear history of navigation actions.
17
17
  *
18
- * Uses two-level expansion pattern matching network/storage components.
18
+ * Tapping a row opens the full-screen detail view (parity with the Events
19
+ * tool) via onSelectEvent — rows no longer expand inline.
19
20
  */
20
21
 
21
22
  function RouteEventsTimeline({
22
23
  events,
23
24
  visitCounts,
24
- onNavigate
25
+ onNavigate,
26
+ onSelectEvent
25
27
  }) {
26
- const [expandedIndex, setExpandedIndex] = (0, _react.useState)(null);
27
28
  const handleItemPress = (0, _react.useCallback)(index => {
28
- setExpandedIndex(prev => prev === index ? null : index);
29
- }, []);
29
+ const event = events[index];
30
+ if (!event) return;
31
+ onSelectEvent(event, visitCounts.get(index) || 1);
32
+ }, [events, visitCounts, onSelectEvent]);
30
33
  if (events.length === 0) {
31
34
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
32
35
  style: styles.emptyState,
@@ -46,8 +49,7 @@ function RouteEventsTimeline({
46
49
  /*#__PURE__*/
47
50
  // Virtualized — the previous ScrollView + map mounted ALL events (at
48
51
  // the 500-event cap that measured 32.8 JS FPS during an idle dwell,
49
- // tool-bench 2026-07-06). extraData re-renders visible rows when the
50
- // expanded row changes.
52
+ // tool-bench 2026-07-06).
51
53
  (0, _jsxRuntime.jsx)(_reactNative.FlatList, {
52
54
  contentContainerStyle: styles.listContent,
53
55
  data: events
@@ -56,14 +58,13 @@ function RouteEventsTimeline({
56
58
  // route events on every bounce; tool-bench 2026-07-06).
57
59
  ,
58
60
  keyExtractor: event => `${event.timestamp}-${event.pathname}`,
59
- extraData: expandedIndex,
60
61
  renderItem: ({
61
62
  item: event,
62
63
  index
63
64
  }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
64
65
  event: event,
65
66
  visitNumber: visitCounts.get(index) || 1,
66
- isExpanded: expandedIndex === index,
67
+ isExpanded: false,
67
68
  index: index,
68
69
  onPress: handleItemPress,
69
70
  onNavigate: onNavigate
@@ -9,6 +9,12 @@ Object.defineProperty(exports, "NavigationStack", {
9
9
  return _NavigationStack.NavigationStack;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "RouteEventDetail", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _RouteEventDetail.RouteEventDetail;
16
+ }
17
+ });
12
18
  Object.defineProperty(exports, "RouteEventExpandedContent", {
13
19
  enumerable: true,
14
20
  get: function () {
@@ -130,6 +136,7 @@ var _RoutesSitemap = require("./components/RoutesSitemap");
130
136
  var _NavigationStack = require("./components/NavigationStack");
131
137
  var _RouteEventItemCompact = require("./components/RouteEventItemCompact");
132
138
  var _RouteEventExpandedContent = require("./components/RouteEventExpandedContent");
139
+ var _RouteEventDetail = require("./components/RouteEventDetail");
133
140
  var _useRouteObserver = require("./useRouteObserver");
134
141
  var _useRouteObserverReactNavigation = require("./useRouteObserverReactNavigation");
135
142
  var _useRouteEvents = require("./hooks/useRouteEvents");
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getRouteTemplate = getRouteTemplate;
7
+ /**
8
+ * Infer a route template from a concrete pathname and its segments.
9
+ * Dynamic segments (numeric ids, UUIDs) collapse to `[id]`, so
10
+ * `/products/42` becomes `/products/[id]`. Returns null when the template
11
+ * matches the pathname (i.e. the route is fully static).
12
+ */
13
+ function getRouteTemplate(pathname, segments) {
14
+ if (!segments || segments.length === 0) return null;
15
+ const templateParts = segments.map(segment => {
16
+ // Numeric ids
17
+ if (/^\d+$/.test(segment)) {
18
+ return "[id]";
19
+ }
20
+ // UUIDs
21
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
22
+ return "[id]";
23
+ }
24
+ return segment;
25
+ });
26
+ const template = "/" + templateParts.join("/");
27
+ return template !== pathname ? template : null;
28
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * RouteEventDetail - Full-screen detail view for a single route event.
5
+ *
6
+ * Mirrors how the Events tool shows a detail page for every other event
7
+ * source: the readable route sections (reusing RouteEventExpandedContent)
8
+ * plus a prominent "Go to route" action. The back header and toolbar copy
9
+ * button are owned by the surrounding modal.
10
+ */
11
+
12
+ import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
13
+ import { Navigation, buoyColors } from "@buoy-gg/shared-ui";
14
+ import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
15
+ import { getRouteTemplate } from "../utils/routeTemplate";
16
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ export function RouteEventDetail({
18
+ event,
19
+ visitNumber = 1,
20
+ onNavigate
21
+ }) {
22
+ const routeTemplate = getRouteTemplate(event.pathname, event.segments);
23
+ return /*#__PURE__*/_jsxs(View, {
24
+ style: styles.container,
25
+ children: [/*#__PURE__*/_jsx(ScrollView, {
26
+ style: styles.scroll,
27
+ contentContainerStyle: styles.scrollContent,
28
+ showsVerticalScrollIndicator: true,
29
+ children: /*#__PURE__*/_jsx(RouteEventExpandedContent, {
30
+ event: event,
31
+ visitNumber: visitNumber,
32
+ routeTemplate: routeTemplate
33
+ })
34
+ }), onNavigate && /*#__PURE__*/_jsx(View, {
35
+ style: styles.footer,
36
+ children: /*#__PURE__*/_jsxs(TouchableOpacity, {
37
+ style: styles.goButton,
38
+ onPress: () => onNavigate(event.pathname),
39
+ activeOpacity: 0.8,
40
+ children: [/*#__PURE__*/_jsx(Navigation, {
41
+ size: 16,
42
+ color: "#FFFFFF"
43
+ }), /*#__PURE__*/_jsx(Text, {
44
+ style: styles.goButtonText,
45
+ children: "Go to route"
46
+ })]
47
+ })
48
+ })]
49
+ });
50
+ }
51
+ const styles = StyleSheet.create({
52
+ container: {
53
+ flex: 1,
54
+ backgroundColor: buoyColors.base
55
+ },
56
+ scroll: {
57
+ flex: 1
58
+ },
59
+ scrollContent: {
60
+ padding: 14
61
+ },
62
+ footer: {
63
+ paddingHorizontal: 16,
64
+ paddingVertical: 12,
65
+ backgroundColor: buoyColors.base,
66
+ borderTopWidth: 1,
67
+ borderTopColor: buoyColors.border
68
+ },
69
+ goButton: {
70
+ flexDirection: "row",
71
+ alignItems: "center",
72
+ justifyContent: "center",
73
+ gap: 8,
74
+ backgroundColor: buoyColors.primary,
75
+ paddingVertical: 12,
76
+ borderRadius: 8
77
+ },
78
+ goButtonText: {
79
+ fontSize: 13,
80
+ fontWeight: "600",
81
+ color: "#FFFFFF",
82
+ fontFamily: "monospace",
83
+ textTransform: "uppercase",
84
+ letterSpacing: 0.5
85
+ }
86
+ });
@@ -10,26 +10,10 @@ import { memo, useMemo } from "react";
10
10
  import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
11
11
  import { CompactRow, RelativeTime, buoyColors } from "@buoy-gg/shared-ui";
12
12
  import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
13
+ import { getRouteTemplate } from "../utils/routeTemplate";
13
14
 
14
15
  // Route type for color coding
15
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
- // Infer route template from pathname and segments
17
- function getRouteTemplate(pathname, segments) {
18
- if (!segments || segments.length === 0) return null;
19
- const templateParts = segments.map(segment => {
20
- // Check if this segment appears to be a dynamic parameter
21
- if (/^\d+$/.test(segment)) {
22
- return "[id]";
23
- }
24
- if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
25
- return "[id]";
26
- }
27
- return segment;
28
- });
29
- const template = "/" + templateParts.join("/");
30
- return template !== pathname ? template : null;
31
- }
32
-
33
17
  // Get route type for color coding
34
18
  function getRouteType(event) {
35
19
  if (event.pathname === "/") {
@@ -16,6 +16,7 @@ import { useIsPro } from "@buoy-gg/license";
16
16
  // Free tier limit for route events
17
17
  const FREE_TIER_EVENT_LIMIT = 3;
18
18
  import { useRouteEvents } from "../hooks/useRouteEvents";
19
+ import { RouteEventDetail } from "./RouteEventDetail";
19
20
  import { RouteFilterViewV2 } from "./RouteFilterViewV2";
20
21
  import { RoutesSitemap } from "./RoutesSitemap";
21
22
  import { ReactNavigationRoutes } from "./ReactNavigationRoutes";
@@ -116,6 +117,10 @@ export function RouteEventsModalWithTabs({
116
117
  // isListening = enabled and store is listening
117
118
  const isListening = isListeningEnabled && isStoreListening;
118
119
  const [showFilters, setShowFilters] = useState(false);
120
+ // Selected event drives the full-screen detail view (parity with the Events
121
+ // tool). null = list view. visitNumber feeds the "Visited N times" section.
122
+ const [selectedEvent, setSelectedEvent] = useState(null);
123
+ const [selectedVisitNumber, setSelectedVisitNumber] = useState(1);
119
124
  const [searchQuery, setSearchQuery] = useState("");
120
125
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set(["/_sitemap", "/api", "/__dev"]));
121
126
  const lastEventRef = useRef(null);
@@ -282,6 +287,18 @@ export function RouteEventsModalWithTabs({
282
287
  const handleToggleFilters = useCallback(() => {
283
288
  setShowFilters(!showFilters);
284
289
  }, [showFilters]);
290
+ const handleSelectEvent = useCallback((event, visitNumber) => {
291
+ setSelectedEvent(event);
292
+ setSelectedVisitNumber(visitNumber);
293
+ }, []);
294
+ const handleBackFromDetail = useCallback(() => {
295
+ setSelectedEvent(null);
296
+ }, []);
297
+ const handleTabChange = useCallback(tab => {
298
+ // Leaving the detail view whenever the tab changes.
299
+ setSelectedEvent(null);
300
+ setActiveTab(tab);
301
+ }, []);
285
302
 
286
303
  // Get all unique pathnames from events
287
304
  const allEventPathnames = useMemo(() => {
@@ -374,6 +391,14 @@ export function RouteEventsModalWithTabs({
374
391
  // No footer needed since we're showing a timeline
375
392
  const footerNode = null;
376
393
  const renderContent = () => {
394
+ // Detail view takes priority — a selected event replaces the list.
395
+ if (selectedEvent) {
396
+ return /*#__PURE__*/_jsx(RouteEventDetail, {
397
+ event: selectedEvent,
398
+ visitNumber: selectedVisitNumber,
399
+ onNavigate: handleNavigate
400
+ });
401
+ }
377
402
  if (activeTab === "routes") {
378
403
  return hasExpoRouter ? /*#__PURE__*/_jsx(RoutesSitemap, {
379
404
  style: styles.contentWrapper
@@ -451,7 +476,8 @@ export function RouteEventsModalWithTabs({
451
476
  }), /*#__PURE__*/_jsx(RouteEventsTimeline, {
452
477
  events: visibleEvents,
453
478
  visitCounts: visitCounts,
454
- onNavigate: handleNavigate
479
+ onNavigate: handleNavigate,
480
+ onSelectEvent: handleSelectEvent
455
481
  })]
456
482
  });
457
483
  };
@@ -463,7 +489,27 @@ export function RouteEventsModalWithTabs({
463
489
  persistenceKey: persistenceKey,
464
490
  header: {
465
491
  showToggleButton: true,
466
- customContent: showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
492
+ customContent: selectedEvent ? /*#__PURE__*/_jsxs(ModalHeader, {
493
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
494
+ onBack: handleBackFromDetail
495
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
496
+ title: selectedEvent.pathname
497
+ }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
498
+ children: /*#__PURE__*/_jsx(ToolbarCopyButton, {
499
+ value: {
500
+ pathname: selectedEvent.pathname,
501
+ previousPathname: selectedEvent.previousPathname,
502
+ params: selectedEvent.params,
503
+ segments: selectedEvent.segments,
504
+ timestamp: selectedEvent.timestamp,
505
+ timestampISO: new Date(selectedEvent.timestamp).toISOString(),
506
+ timeSincePrevious: selectedEvent.timeSincePrevious,
507
+ visitNumber: selectedVisitNumber
508
+ },
509
+ buttonStyle: styles.iconButton
510
+ })
511
+ })]
512
+ }) : showFilters ? /*#__PURE__*/_jsxs(ModalHeader, {
467
513
  children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
468
514
  onBack: () => setShowFilters(false)
469
515
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
@@ -487,7 +533,7 @@ export function RouteEventsModalWithTabs({
487
533
  label: "Stack"
488
534
  }],
489
535
  activeTab: activeTab,
490
- onTabChange: tab => setActiveTab(tab)
536
+ onTabChange: tab => handleTabChange(tab)
491
537
  })
492
538
  }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
493
539
  children: [activeTab === "stack" && /*#__PURE__*/_jsx(ToolbarCopyButton, {
@@ -6,10 +6,11 @@
6
6
  * Shows events in the order they happened (most recent first),
7
7
  * providing a clear history of navigation actions.
8
8
  *
9
- * Uses two-level expansion pattern matching network/storage components.
9
+ * Tapping a row opens the full-screen detail view (parity with the Events
10
+ * tool) via onSelectEvent — rows no longer expand inline.
10
11
  */
11
12
 
12
- import { useCallback, useState } from "react";
13
+ import { useCallback } from "react";
13
14
  import { View, Text, FlatList, StyleSheet } from "react-native";
14
15
  import { Navigation, buoyColors } from "@buoy-gg/shared-ui";
15
16
  import { RouteEventItemCompact } from "./RouteEventItemCompact";
@@ -17,12 +18,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
18
  export function RouteEventsTimeline({
18
19
  events,
19
20
  visitCounts,
20
- onNavigate
21
+ onNavigate,
22
+ onSelectEvent
21
23
  }) {
22
- const [expandedIndex, setExpandedIndex] = useState(null);
23
24
  const handleItemPress = useCallback(index => {
24
- setExpandedIndex(prev => prev === index ? null : index);
25
- }, []);
25
+ const event = events[index];
26
+ if (!event) return;
27
+ onSelectEvent(event, visitCounts.get(index) || 1);
28
+ }, [events, visitCounts, onSelectEvent]);
26
29
  if (events.length === 0) {
27
30
  return /*#__PURE__*/_jsxs(View, {
28
31
  style: styles.emptyState,
@@ -42,8 +45,7 @@ export function RouteEventsTimeline({
42
45
  /*#__PURE__*/
43
46
  // Virtualized — the previous ScrollView + map mounted ALL events (at
44
47
  // the 500-event cap that measured 32.8 JS FPS during an idle dwell,
45
- // tool-bench 2026-07-06). extraData re-renders visible rows when the
46
- // expanded row changes.
48
+ // tool-bench 2026-07-06).
47
49
  _jsx(FlatList, {
48
50
  contentContainerStyle: styles.listContent,
49
51
  data: events
@@ -52,14 +54,13 @@ export function RouteEventsTimeline({
52
54
  // route events on every bounce; tool-bench 2026-07-06).
53
55
  ,
54
56
  keyExtractor: event => `${event.timestamp}-${event.pathname}`,
55
- extraData: expandedIndex,
56
57
  renderItem: ({
57
58
  item: event,
58
59
  index
59
60
  }) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
60
61
  event: event,
61
62
  visitNumber: visitCounts.get(index) || 1,
62
- isExpanded: expandedIndex === index,
63
+ isExpanded: false,
63
64
  index: index,
64
65
  onPress: handleItemPress,
65
66
  onNavigate: onNavigate
@@ -30,6 +30,7 @@ export { RoutesSitemap } from "./components/RoutesSitemap";
30
30
  export { NavigationStack } from "./components/NavigationStack";
31
31
  export { RouteEventItemCompact } from "./components/RouteEventItemCompact";
32
32
  export { RouteEventExpandedContent } from "./components/RouteEventExpandedContent";
33
+ export { RouteEventDetail } from "./components/RouteEventDetail";
33
34
  // =============================================================================
34
35
  // HOOKS (For consuming route data)
35
36
  // =============================================================================
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Infer a route template from a concrete pathname and its segments.
5
+ * Dynamic segments (numeric ids, UUIDs) collapse to `[id]`, so
6
+ * `/products/42` becomes `/products/[id]`. Returns null when the template
7
+ * matches the pathname (i.e. the route is fully static).
8
+ */
9
+ export function getRouteTemplate(pathname, segments) {
10
+ if (!segments || segments.length === 0) return null;
11
+ const templateParts = segments.map(segment => {
12
+ // Numeric ids
13
+ if (/^\d+$/.test(segment)) {
14
+ return "[id]";
15
+ }
16
+ // UUIDs
17
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment)) {
18
+ return "[id]";
19
+ }
20
+ return segment;
21
+ });
22
+ const template = "/" + templateParts.join("/");
23
+ return template !== pathname ? template : null;
24
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * RouteEventDetail - Full-screen detail view for a single route event.
3
+ *
4
+ * Mirrors how the Events tool shows a detail page for every other event
5
+ * source: the readable route sections (reusing RouteEventExpandedContent)
6
+ * plus a prominent "Go to route" action. The back header and toolbar copy
7
+ * button are owned by the surrounding modal.
8
+ */
9
+ import type { RouteChangeEvent } from "../RouteObserver";
10
+ export interface RouteEventDetailProps {
11
+ event: RouteChangeEvent;
12
+ /** How many times this route has been visited (hides "Visited N times" when <= 1). */
13
+ visitNumber?: number;
14
+ /** Navigate the host app to this route. When omitted the action button is hidden. */
15
+ onNavigate?: (pathname: string) => void;
16
+ }
17
+ export declare function RouteEventDetail({ event, visitNumber, onNavigate, }: RouteEventDetailProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=RouteEventDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouteEventDetail.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventDetail.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIzD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,gBAAgB,CAAC;IACxB,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qFAAqF;IACrF,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAe,EACf,UAAU,GACX,EAAE,qBAAqB,+BA+BvB"}