@buoy-gg/route-events 7.0.0 → 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.
- package/lib/commonjs/components/RouteEventDetail.js +90 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +3 -19
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +49 -3
- package/lib/commonjs/components/RouteEventsTimeline.js +10 -9
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/utils/routeTemplate.js +28 -0
- package/lib/module/components/RouteEventDetail.js +86 -0
- package/lib/module/components/RouteEventItemCompact.js +1 -17
- package/lib/module/components/RouteEventsModalWithTabs.js +49 -3
- package/lib/module/components/RouteEventsTimeline.js +11 -10
- package/lib/module/index.js +1 -0
- package/lib/module/utils/routeTemplate.js +24 -0
- package/lib/typescript/components/RouteEventDetail.d.ts +18 -0
- package/lib/typescript/components/RouteEventDetail.d.ts.map +1 -0
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsTimeline.d.ts +4 -2
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/utils/routeTemplate.d.ts +8 -0
- package/lib/typescript/utils/routeTemplate.d.ts.map +1 -0
- package/package.json +5 -5
- package/lib/commonjs/components/RouteEventDetailContent.js +0 -499
- package/lib/module/components/RouteEventDetailContent.js +0 -494
- package/lib/typescript/components/RouteEventDetailContent.d.ts +0 -21
- 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:
|
|
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 =>
|
|
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
|
-
*
|
|
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
|
-
|
|
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).
|
|
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:
|
|
67
|
+
isExpanded: false,
|
|
67
68
|
index: index,
|
|
68
69
|
onPress: handleItemPress,
|
|
69
70
|
onNavigate: onNavigate
|
package/lib/commonjs/index.js
CHANGED
|
@@ -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:
|
|
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 =>
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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).
|
|
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:
|
|
63
|
+
isExpanded: false,
|
|
63
64
|
index: index,
|
|
64
65
|
onPress: handleItemPress,
|
|
65
66
|
onNavigate: onNavigate
|
package/lib/module/index.js
CHANGED
|
@@ -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"}
|