@buoy-gg/route-events 5.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,654 +1,58 @@
1
- # @buoy/route-events
1
+ # @buoy-gg/route-events
2
2
 
3
- A comprehensive route tracking and visualization toolkit for Expo Router applications. Monitor route changes, inspect navigation state, explore your route sitemap, and visualize the navigation stack.
3
+ [![npm version](https://img.shields.io/npm/v/@buoy-gg/route-events?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/route-events) [![npm downloads](https://img.shields.io/npm/dm/@buoy-gg/route-events?style=flat-square&labelColor=1c1c1c&color=10B981)](https://www.npmjs.com/package/@buoy-gg/route-events)
4
4
 
5
- ## Features
5
+ **See every route in your app, watch navigation happen in real time, and jump to any screen from a full sitemap.**
6
6
 
7
- - **Route Event Tracking**: Automatically track all route changes with detailed event information
8
- - **Route Sitemap**: Browse all available routes in your app with search and navigation
9
- - **Navigation Stack Viewer**: Inspect the current navigation stack in real-time
10
- - **Event Timeline**: View a chronological timeline of all route changes
11
- - **Filtering & Search**: Filter events by pathname patterns and search through routes
12
- - **Persistent State**: Remembers your monitoring preferences and filters
13
- - **Full Modal UI**: Beautiful, production-ready modal interface for all features
7
+ Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside your React Native app. Install it and it auto-appears in the floating menu from [`@buoy-gg/core`](https://www.npmjs.com/package/@buoy-gg/core).
14
8
 
15
- ## Installation
9
+ ![Route Inspector demo](https://github.com/user-attachments/assets/90e55dc7-f8ab-423a-9770-84b9ff9c8446)
16
10
 
17
- This package is part of the React Buoy monorepo and is automatically available to other packages and the example app.
18
-
19
- For external projects:
11
+ ## Install
20
12
 
21
13
  ```bash
22
- npm install @buoy/route-events
23
- # or
24
- pnpm add @buoy/route-events
25
- # or
26
- yarn add @buoy/route-events
27
- ```
28
-
29
- ## Quick Start
30
-
31
- ### Simplest Setup - Just 1 Line!
32
-
33
- **Import the preset and add it to your tools array. Done!**
34
-
35
- ```typescript
36
- import { routeEventsToolPreset } from '@buoy/route-events';
37
- import { FloatingDevTools } from '@buoy/core';
38
-
39
- const installedApps = [
40
- routeEventsToolPreset, // That's it! One line.
41
- // ...your other tools
42
- ];
43
-
44
- function App() {
45
- return (
46
- <FloatingDevTools
47
- apps={installedApps}
48
- environment="local"
49
- userRole="admin"
50
- />
51
- );
52
- }
14
+ npm install @buoy-gg/core @buoy-gg/route-events
53
15
  ```
54
16
 
55
- **Done!** The preset automatically:
56
- - ✅ Starts tracking routes when opened
57
- - ✅ Uses the built-in route observer singleton
58
- - ✅ Provides all three tabs (Routes, Events, Stack)
59
- - ✅ No configuration, no props, no hooks to call
60
-
61
- ## Requirements
17
+ ## Quick start
62
18
 
63
- `@buoy/route-events` relies on the same core libraries as Expo Router itself:
64
-
65
- - `expo-router` ≥ 2.0.0 (the Routes tab reads `store.routeNode` directly)
66
- - `@react-navigation/native` ≥ 7.x (Stack tab + Expo Router routing hooks)
67
- - `@react-native-async-storage/async-storage` (state persistence)
68
- - `react` / `react-native`
69
-
70
- These packages are declared as `peerDependencies`, so your app decides the exact versions. When a dependency is missing or not initialized, the devtool logs a clear `[RouteEvents] ...` console message (in dev builds) and gracefully disables the affected feature instead of crashing. See `packages/route-events/docs/ROUTES_SITEMAP_ACCESS_GUIDE.md` for the full data-flow and troubleshooting guide.
71
-
72
- ### Alternative: Manual Setup
73
-
74
- If you're not using FloatingDevTools or want more control:
75
-
76
- ```typescript
77
- import { RouteEventsModalWithTabs } from '@buoy/route-events';
78
-
79
- function App() {
80
- const [showRoutes, setShowRoutes] = useState(false);
19
+ ```tsx
20
+ // app/_layout.tsx (Expo Router)
21
+ import { Slot } from "expo-router";
22
+ import { FloatingDevTools } from "@buoy-gg/core";
81
23
 
24
+ export default function RootLayout() {
82
25
  return (
83
26
  <>
84
- <Button onPress={() => setShowRoutes(true)}>
85
- Open Route Inspector
86
- </Button>
87
-
88
- <RouteEventsModalWithTabs
89
- visible={showRoutes}
90
- onClose={() => setShowRoutes(false)}
91
- />
27
+ <Slot />
28
+ <FloatingDevTools />
92
29
  </>
93
30
  );
94
31
  }
95
32
  ```
96
33
 
97
- ## API Reference
98
-
99
- ### Core Components
100
-
101
- #### `RouteEventsModalWithTabs`
102
-
103
- The main modal interface with three tabs: Routes, Events, and Stack.
104
-
105
- **Props:**
106
-
107
- ```typescript
108
- interface RouteEventsModalWithTabsProps {
109
- /** Whether the modal is visible */
110
- visible: boolean;
111
- /** Callback when modal is closed */
112
- onClose: () => void;
113
- /** Optional back button handler */
114
- onBack?: () => void;
115
- /** Whether to use shared modal dimensions */
116
- enableSharedModalDimensions?: boolean;
117
- /**
118
- * Optional route observer instance.
119
- * If not provided, uses the default singleton (recommended).
120
- * Route tracking starts automatically when the modal is opened.
121
- */
122
- routeObserver?: RouteObserver;
123
- }
124
- ```
125
-
126
- **Example:**
127
-
128
- ```typescript
129
- <RouteEventsModalWithTabs
130
- visible={isVisible}
131
- onClose={handleClose}
132
- />
133
- ```
134
-
135
- **Advanced Example (with custom observer):**
136
-
137
- Only needed if you want to use a custom route observer instance:
138
-
139
- ```typescript
140
- import { RouteEventsModalWithTabs, routeObserver } from '@buoy/route-events';
141
-
142
- <RouteEventsModalWithTabs
143
- visible={isVisible}
144
- onClose={handleClose}
145
- routeObserver={routeObserver} // Optional - uses default if not provided
146
- />
147
- ```
148
-
149
- #### `RoutesSitemap`
150
-
151
- Standalone component for browsing all routes in your application.
152
-
153
- **Props:**
154
-
155
- ```typescript
156
- interface RoutesSitemapProps {
157
- style?: StyleProp<ViewStyle>;
158
- }
159
- ```
160
-
161
- **Example:**
162
-
163
- ```typescript
164
- import { RoutesSitemap } from '@buoy/route-events';
165
-
166
- function RoutesScreen() {
167
- return <RoutesSitemap />;
168
- }
169
- ```
170
-
171
- #### `NavigationStack`
172
-
173
- Standalone component for visualizing the current navigation stack.
174
-
175
- **Props:**
176
-
177
- ```typescript
178
- interface NavigationStackProps {
179
- style?: StyleProp<ViewStyle>;
180
- }
181
- ```
182
-
183
- **Example:**
184
-
185
- ```typescript
186
- import { NavigationStack } from '@buoy/route-events';
187
-
188
- function DebugScreen() {
189
- return <NavigationStack />;
190
- }
191
- ```
192
-
193
- ### Hooks
194
-
195
- #### `useRouteObserver`
196
-
197
- Hook to track route changes and emit events to the global observer.
198
-
199
- **Note:** You typically don't need to use this hook directly - the `RouteEventsModalWithTabs` component starts tracking automatically. This hook is only needed for advanced use cases like custom analytics.
200
-
201
- **Signature:**
202
-
203
- ```typescript
204
- function useRouteObserver(
205
- callback?: (event: RouteChangeEvent) => void
206
- ): void
207
- ```
208
-
209
- **Examples:**
210
-
211
- ```typescript
212
- // Only needed for custom analytics - modal handles tracking automatically!
213
- import { useRouteObserver } from '@buoy/route-events';
214
-
215
- export default function RootLayout() {
216
- useRouteObserver((event) => {
217
- analytics.trackPageView(event.pathname);
218
- });
219
-
220
- return <Stack />;
221
- }
222
- ```
223
-
224
- #### `useRouteSitemap`
225
-
226
- Access route information from your app's file-based routing structure.
227
-
228
- **Signature:**
229
-
230
- ```typescript
231
- function useRouteSitemap(
232
- options?: UseRouteSitemapOptions
233
- ): UseRouteSitemapResult
234
- ```
235
-
236
- **Options:**
237
-
238
- ```typescript
239
- interface UseRouteSitemapOptions {
240
- /** Whether to include layout routes */
241
- includeLayouts?: boolean;
242
- /** Whether to include route groups */
243
- includeGroups?: boolean;
244
- }
245
- ```
246
-
247
- **Result:**
248
-
249
- ```typescript
250
- interface UseRouteSitemapResult {
251
- /** All routes in the application */
252
- routes: RouteInfo[];
253
- /** Routes grouped by directory */
254
- groups: RouteGroup[];
255
- /** Statistics about the routes */
256
- stats: RouteStats;
257
- /** Get a specific route by path */
258
- getRoute: (path: string) => RouteInfo | undefined;
259
- /** Get all parent routes of a path */
260
- getParents: (path: string) => RouteInfo[];
261
- }
262
- ```
263
-
264
- **Example:**
265
-
266
- ```typescript
267
- import { useRouteSitemap } from '@buoy/route-events';
268
-
269
- function RouteDebugger() {
270
- const { routes, stats, getRoute } = useRouteSitemap();
271
-
272
- console.log(`Total routes: ${stats.total}`);
273
- console.log(`Dynamic routes: ${stats.byType.dynamic}`);
274
-
275
- const homeRoute = getRoute('/');
276
- // { path: '/', type: 'index', ... }
277
-
278
- return (
279
- <View>
280
- {routes.map(route => (
281
- <Text key={route.path}>{route.path}</Text>
282
- ))}
283
- </View>
284
- );
285
- }
286
- ```
287
-
288
- #### `useNavigationStack`
289
-
290
- Access and control the current navigation stack.
34
+ That's it. `FloatingDevTools` detects the package, mounts the route tracker for you, and the ROUTES tool shows up in the menu. It auto-detects whether you're on Expo Router or React Navigation and adapts.
291
35
 
292
- **Signature:**
36
+ ## What you get
293
37
 
294
- ```typescript
295
- function useNavigationStack(): UseNavigationStackResult
296
- ```
38
+ - **Full sitemap** — browse every route in your app, searchable, with route types (static, dynamic, layout) labeled
39
+ - **Jump to any screen** — tap a route in the sitemap and navigate straight to it, params and all
40
+ - **Live navigation stack** — watch the stack push, pop, and replace as you move through the app
41
+ - **Navigation event timeline** — every navigation logged with path, params, timestamp, and time since the previous one
42
+ - **Filtering & search** — filter events by pathname pattern, search across routes
43
+ - **Zero setup** — no `<RouteTracker />` to place, no config; the core menu wires it automatically
44
+ - **Hooks for custom UIs** — `useRouteEvents`, `useRouteSitemap`, and `useNavigationStack` if you want the data without the modal
297
45
 
298
- **Result:**
299
-
300
- ```typescript
301
- interface UseNavigationStackResult {
302
- /** The current navigation stack */
303
- stack: StackDisplayItem[];
304
- /** Whether the data is loading */
305
- loading: boolean;
306
- /** Navigate to a specific stack index */
307
- navigateToIndex: (index: number) => void;
308
- /** Pop back to a specific index */
309
- popToIndex: (index: number) => void;
310
- /** Go back one screen */
311
- goBack: () => void;
312
- /** Go to the root of the stack */
313
- popToTop: () => void;
314
- }
315
- ```
316
-
317
- **Example:**
318
-
319
- ```typescript
320
- import { useNavigationStack } from '@buoy/route-events';
321
-
322
- function NavigationDebugger() {
323
- const { stack, goBack, popToTop } = useNavigationStack();
324
-
325
- return (
326
- <View>
327
- <Text>Stack Depth: {stack.length}</Text>
328
- <Button onPress={goBack}>Go Back</Button>
329
- <Button onPress={popToTop}>Go to Root</Button>
330
-
331
- {stack.map((item, index) => (
332
- <Text key={item.key}>
333
- {index}: {item.displayPath}
334
- </Text>
335
- ))}
336
- </View>
337
- );
338
- }
339
- ```
340
-
341
- ### Utilities
342
-
343
- #### `RouteObserver`
344
-
345
- Singleton instance for observing route changes.
346
-
347
- **Methods:**
348
-
349
- ```typescript
350
- class RouteObserver {
351
- /** Emit a route change event */
352
- emit(event: RouteChangeEvent): void;
353
-
354
- /** Add a listener for route changes */
355
- addListener(callback: (event: RouteChangeEvent) => void): () => void;
356
-
357
- /** Remove a listener */
358
- removeListener(callback: (event: RouteChangeEvent) => void): void;
359
- }
360
- ```
361
-
362
- **Example:**
363
-
364
- ```typescript
365
- import { routeObserver } from '@buoy/route-events';
366
-
367
- // Add listener
368
- const unsubscribe = routeObserver.addListener((event) => {
369
- console.log('Route changed:', event.pathname);
370
- });
371
-
372
- // Later, remove listener
373
- unsubscribe();
374
- ```
375
-
376
- #### `RouteParser`
377
-
378
- Utility for parsing Expo Router's route structure.
379
-
380
- **Methods:**
381
-
382
- ```typescript
383
- class RouteParser {
384
- /** Get all routes from the router */
385
- static getRoutes(options?: {
386
- includeLayouts?: boolean;
387
- includeGroups?: boolean;
388
- }): RouteInfo[];
389
-
390
- /** Group routes by directory */
391
- static groupRoutes(routes: RouteInfo[]): RouteGroup[];
392
-
393
- /** Get statistics about routes */
394
- static getStats(routes: RouteInfo[]): RouteStats;
395
- }
396
- ```
397
-
398
- ### Types
399
-
400
- #### `RouteChangeEvent`
401
-
402
- ```typescript
403
- interface RouteChangeEvent {
404
- /** Current pathname */
405
- pathname: string;
406
- /** Route parameters */
407
- params: Record<string, string | string[]>;
408
- /** Route segments */
409
- segments: string[];
410
- /** Timestamp of the change */
411
- timestamp: number;
412
- /** Previous pathname (if available) */
413
- previousPathname?: string;
414
- /** Time since previous navigation in ms */
415
- timeSincePrevious?: number;
416
- }
417
- ```
418
-
419
- #### `RouteInfo`
420
-
421
- ```typescript
422
- interface RouteInfo {
423
- /** Full path of the route */
424
- path: string;
425
- /** Route type */
426
- type: RouteType;
427
- /** Route name/file */
428
- name: string;
429
- /** Dynamic parameters */
430
- params: string[];
431
- /** Whether it's a layout */
432
- isLayout: boolean;
433
- /** Whether it's a route group */
434
- isGroup: boolean;
435
- /** Parent path */
436
- parent?: string;
437
- }
438
- ```
439
-
440
- #### `RouteType`
441
-
442
- ```typescript
443
- type RouteType =
444
- | 'index' // index routes (/)
445
- | 'static' // static routes (/about)
446
- | 'dynamic' // dynamic routes (/user/[id])
447
- | 'catch-all' // catch-all routes (/docs/[...path])
448
- | 'layout' // layout routes (_layout.tsx)
449
- | 'group'; // route groups ((tabs))
450
- ```
451
-
452
- ## Use Cases
453
-
454
- ### Analytics Integration
455
-
456
- ```typescript
457
- import { useRouteObserver } from '@buoy/route-events';
458
-
459
- export default function RootLayout() {
460
- useRouteObserver((event) => {
461
- // Track page views
462
- analytics.trackPageView({
463
- path: event.pathname,
464
- params: event.params,
465
- previousPath: event.previousPathname,
466
- timeSpent: event.timeSincePrevious,
467
- });
468
- });
469
-
470
- return <Stack />;
471
- }
472
- ```
473
-
474
- ### Performance Monitoring
475
-
476
- ```typescript
477
- import { useRouteObserver } from '@buoy/route-events';
478
-
479
- export default function RootLayout() {
480
- useRouteObserver((event) => {
481
- if (event.timeSincePrevious && event.timeSincePrevious > 1000) {
482
- // Log slow navigations
483
- console.warn('Slow navigation:', {
484
- from: event.previousPathname,
485
- to: event.pathname,
486
- duration: event.timeSincePrevious,
487
- });
488
- }
489
- });
490
-
491
- return <Stack />;
492
- }
493
- ```
494
-
495
- ### Development Tools
496
-
497
- ```typescript
498
- import { RouteEventsModalWithTabs, routeObserver } from '@buoy/route-events';
499
-
500
- function DevTools() {
501
- const [visible, setVisible] = useState(false);
502
-
503
- // Only show in development
504
- if (__DEV__) {
505
- return (
506
- <>
507
- <TouchableOpacity
508
- style={styles.devButton}
509
- onPress={() => setVisible(true)}
510
- >
511
- <Text>🧭 Routes</Text>
512
- </TouchableOpacity>
513
-
514
- <RouteEventsModalWithTabs
515
- visible={visible}
516
- onClose={() => setVisible(false)}
517
- routeObserver={routeObserver}
518
- />
519
- </>
520
- );
521
- }
522
-
523
- return null;
524
- }
525
- ```
526
-
527
- ### Route Documentation
528
-
529
- ```typescript
530
- import { useRouteSitemap } from '@buoy/route-events';
531
-
532
- function RouteDocumentation() {
533
- const { routes, groups, stats } = useRouteSitemap({
534
- includeLayouts: false,
535
- includeGroups: false,
536
- });
537
-
538
- return (
539
- <ScrollView>
540
- <Text>Total Routes: {stats.total}</Text>
541
-
542
- {groups.map(group => (
543
- <View key={group.name}>
544
- <Text>{group.name} ({group.routes.length})</Text>
545
- {group.routes.map(route => (
546
- <Text key={route.path}>
547
- {route.path} - {route.type}
548
- </Text>
549
- ))}
550
- </View>
551
- ))}
552
- </ScrollView>
553
- );
554
- }
555
- ```
556
-
557
- ## Features in Detail
558
-
559
- ### Routes Tab
560
-
561
- - Browse all routes in your application
562
- - Search routes by path or name
563
- - View route types (static, dynamic, catch-all)
564
- - Navigate to any route directly
565
- - Handles dynamic route parameters with prompts
566
- - Group and layout route identification
567
-
568
- ### Events Tab
569
-
570
- - Real-time route change monitoring
571
- - Toggle monitoring on/off
572
- - Event timeline with timestamps
573
- - Filter events by pathname patterns
574
- - Search through event history
575
- - View detailed event information
576
- - Compare route changes (diff view)
577
- - Clear event history
578
- - Persistent filter preferences
579
-
580
- ### Stack Tab
581
-
582
- - Real-time navigation stack visualization
583
- - Stack depth indicator
584
- - Navigate to any stack level
585
- - Pop to specific stack index
586
- - Quick navigation controls
587
- - Route parameter display
588
-
589
- ## Dependencies
590
-
591
- - `@buoy/shared-ui` - Common UI components and utilities
592
- - `expo-router` - Expo Router integration (peer dependency)
593
- - `@react-navigation/native` - React Navigation integration (peer dependency)
594
- - `@react-native-async-storage/async-storage` - For persistent preferences (peer dependency)
595
- - React and React Native (peer dependencies)
596
-
597
- ## Development
598
-
599
- ### Building
600
-
601
- ```bash
602
- pnpm build
603
- ```
604
-
605
- ### Type Checking
606
-
607
- ```bash
608
- pnpm typecheck
609
- ```
610
-
611
- ### Clean Build
612
-
613
- ```bash
614
- pnpm clean
615
- ```
616
-
617
- ## Package Structure
618
-
619
- ```
620
- route-events/
621
- ├── src/
622
- │ ├── components/ # UI components
623
- │ │ ├── RouteEventsModalWithTabs.tsx
624
- │ │ ├── RoutesSitemap.tsx
625
- │ │ ├── NavigationStack.tsx
626
- │ │ ├── RouteEventsTimeline.tsx
627
- │ │ ├── RouteEventItemCompact.tsx
628
- │ │ ├── RouteEventDetailContent.tsx
629
- │ │ ├── RouteEventExpandedContent.tsx
630
- │ │ └── RouteFilterViewV2.tsx
631
- │ ├── RouteObserver.ts # Event system
632
- │ ├── RouteParser.ts # Route parsing utilities
633
- │ ├── useRouteObserver.ts # Route tracking hook
634
- │ ├── useRouteSitemap.ts # Sitemap access hook
635
- │ ├── useNavigationStack.ts # Stack access hook
636
- │ └── index.tsx # Main exports
637
- ├── lib/ # Built output (git ignored)
638
- ├── package.json
639
- ├── tsconfig.json
640
- ├── tsconfig.build.json
641
- └── README.md
642
- ```
46
+ ## Desktop & AI
643
47
 
644
- ## License
48
+ The same live session streams to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) (free, macOS/Windows/Linux) and to Claude Code or Cursor via the [Buoy MCP server](https://buoy.gg/buoy/latest/docs/mcp).
645
49
 
646
- MIT
50
+ ## Free vs Pro
647
51
 
648
- ## Contributing
52
+ Every tool is free. [Pro](https://buoy.gg/pricing) unlocks production builds, the MCP server, and unlimited capture. Every weekend, Pro features unlock free for everyone.
649
53
 
650
- See the main repository [CONTRIBUTING.md](../../CONTRIBUTING.md) for contribution guidelines.
54
+ ---
651
55
 
652
- ## Support
56
+ 📚 [Full docs](https://buoy.gg/buoy/latest/docs/tools/routes) · [All Buoy tools](https://github.com/Buoy-gg/buoy)
653
57
 
654
- For issues and feature requests, please visit the [GitHub repository](https://github.com/LovesWorking/react-native-buoy/issues).
58
+ Proprietary software. © Buoy LLC. [Terms](https://buoy.gg/terms)
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.RouteEventItemCompact = RouteEventItemCompact;
6
+ exports.RouteEventItemCompact = void 0;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
@@ -114,12 +114,17 @@ function getBadgeText(event) {
114
114
  // Don't show badge text - route type is already shown in status label
115
115
  return undefined;
116
116
  }
117
- function RouteEventItemCompact({
117
+
118
+ // Memo justified by profiling (tool-bench 2026-07-06): 400 row renders
119
+ // during a 5s dwell at 500 events (timeline re-renders cascaded into all
120
+ // mounted rows via an inline onPress closure).
121
+ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _react.memo)(function RouteEventItemCompact({
118
122
  event,
119
123
  visitNumber,
120
124
  isExpanded,
121
125
  onPress,
122
- onNavigate
126
+ onNavigate,
127
+ index
123
128
  }) {
124
129
  const routeType = (0, _react.useMemo)(() => getRouteType(event), [event]);
125
130
  const routeTemplate = (0, _react.useMemo)(() => getRouteTemplate(event.pathname, event.segments), [event.pathname, event.segments]);
@@ -127,7 +132,6 @@ function RouteEventItemCompact({
127
132
  const statusLabel = (0, _react.useMemo)(() => getStatusLabel(routeType), [routeType]);
128
133
  const statusSublabel = (0, _react.useMemo)(() => getStatusSublabel(event), [event]);
129
134
  const badgeText = (0, _react.useMemo)(() => getBadgeText(event), [event]);
130
- const timeLabel = (0, _sharedUi.useRelativeTime)(event.timestamp);
131
135
  const expandedContent = (0, _react.useMemo)(() => {
132
136
  if (!isExpanded) return undefined;
133
137
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventExpandedContent.RouteEventExpandedContent, {
@@ -170,14 +174,16 @@ function RouteEventItemCompact({
170
174
  badgeText: onNavigate ? undefined : badgeText,
171
175
  badgeColor: statusColor,
172
176
  customBadge: customBadge,
173
- bottomRightText: timeLabel,
177
+ bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
178
+ timestamp: event.timestamp
179
+ }),
174
180
  showChevron: true,
175
181
  expandedContent: expandedContent,
176
182
  isExpanded: isExpanded,
177
183
  expandedGlowColor: statusColor,
178
- onPress: onPress
184
+ onPress: () => onPress(index)
179
185
  });
180
- }
186
+ });
181
187
  const styles = _reactNative.StyleSheet.create({
182
188
  badgeContainer: {
183
189
  flexDirection: "row",
@@ -42,16 +42,40 @@ function RouteEventsTimeline({
42
42
  })]
43
43
  });
44
44
  }
45
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
46
- contentContainerStyle: styles.listContent,
47
- children: events.map((event, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
48
- event: event,
49
- visitNumber: visitCounts.get(index) || 1,
50
- isExpanded: expandedIndex === index,
51
- onPress: () => handleItemPress(index),
52
- onNavigate: onNavigate
53
- }, `event-${index}-${event.timestamp}`))
54
- });
45
+ return (
46
+ /*#__PURE__*/
47
+ // Virtualized — the previous ScrollView + map mounted ALL events (at
48
+ // 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.
51
+ (0, _jsxRuntime.jsx)(_reactNative.FlatList, {
52
+ contentContainerStyle: styles.listContent,
53
+ data: events
54
+ // STABLE content keys — index-based keys remount every row when a new
55
+ // event prepends (measured 4 JS FPS during automation, which emits
56
+ // route events on every bounce; tool-bench 2026-07-06).
57
+ ,
58
+ keyExtractor: event => `${event.timestamp}-${event.pathname}`,
59
+ extraData: expandedIndex,
60
+ renderItem: ({
61
+ item: event,
62
+ index
63
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_RouteEventItemCompact.RouteEventItemCompact, {
64
+ event: event,
65
+ visitNumber: visitCounts.get(index) || 1,
66
+ isExpanded: expandedIndex === index,
67
+ index: index,
68
+ onPress: handleItemPress,
69
+ onNavigate: onNavigate
70
+ }),
71
+ showsVerticalScrollIndicator: true,
72
+ removeClippedSubviews: true,
73
+ initialNumToRender: 12,
74
+ maxToRenderPerBatch: 8,
75
+ windowSize: 5,
76
+ updateCellsBatchingPeriod: 50
77
+ })
78
+ );
55
79
  }
56
80
  const styles = _reactNative.StyleSheet.create({
57
81
  listContent: {
@@ -51,19 +51,28 @@ function useRouteEvents(options = {}) {
51
51
  setIsListening(false);
52
52
  return;
53
53
  }
54
+
55
+ // Frame-coalesced (see network's useNetworkEventList): navigation
56
+ // bursts land as one commit per frame instead of one per emission.
57
+ let rafId = null;
58
+ let pending = null;
54
59
  const unsubscribe = _routeEventStore.routeEventStore.subscribeToEvents(allEvents => {
55
- // Limit events
56
- if (allEvents.length > maxEvents) {
57
- setEvents(allEvents.slice(0, maxEvents));
58
- } else {
59
- setEvents(allEvents);
60
- }
60
+ pending = allEvents;
61
+ if (rafId != null) return;
62
+ rafId = requestAnimationFrame(() => {
63
+ rafId = null;
64
+ if (!pending) return;
65
+ const latest = pending;
66
+ pending = null;
67
+ setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
68
+ });
61
69
  });
62
70
 
63
71
  // Mark as listening after successful subscription
64
72
  setIsListening(true);
65
73
  return () => {
66
74
  unsubscribe();
75
+ if (rafId != null) cancelAnimationFrame(rafId);
67
76
  // Note: Don't set isListening=false here because we might be
68
77
  // re-subscribing due to dependency change, not actually stopping
69
78
  };
@@ -6,9 +6,9 @@
6
6
  * Uses CompactRow pattern matching network/storage/redux components
7
7
  */
8
8
 
9
- import { useMemo } from "react";
9
+ import { memo, useMemo } from "react";
10
10
  import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
11
- import { CompactRow, useRelativeTime, buoyColors } from "@buoy-gg/shared-ui";
11
+ import { CompactRow, RelativeTime, buoyColors } from "@buoy-gg/shared-ui";
12
12
  import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
13
13
 
14
14
  // Route type for color coding
@@ -110,12 +110,17 @@ function getBadgeText(event) {
110
110
  // Don't show badge text - route type is already shown in status label
111
111
  return undefined;
112
112
  }
113
- export function RouteEventItemCompact({
113
+
114
+ // Memo justified by profiling (tool-bench 2026-07-06): 400 row renders
115
+ // during a 5s dwell at 500 events (timeline re-renders cascaded into all
116
+ // mounted rows via an inline onPress closure).
117
+ export const RouteEventItemCompact = /*#__PURE__*/memo(function RouteEventItemCompact({
114
118
  event,
115
119
  visitNumber,
116
120
  isExpanded,
117
121
  onPress,
118
- onNavigate
122
+ onNavigate,
123
+ index
119
124
  }) {
120
125
  const routeType = useMemo(() => getRouteType(event), [event]);
121
126
  const routeTemplate = useMemo(() => getRouteTemplate(event.pathname, event.segments), [event.pathname, event.segments]);
@@ -123,7 +128,6 @@ export function RouteEventItemCompact({
123
128
  const statusLabel = useMemo(() => getStatusLabel(routeType), [routeType]);
124
129
  const statusSublabel = useMemo(() => getStatusSublabel(event), [event]);
125
130
  const badgeText = useMemo(() => getBadgeText(event), [event]);
126
- const timeLabel = useRelativeTime(event.timestamp);
127
131
  const expandedContent = useMemo(() => {
128
132
  if (!isExpanded) return undefined;
129
133
  return /*#__PURE__*/_jsx(RouteEventExpandedContent, {
@@ -166,14 +170,16 @@ export function RouteEventItemCompact({
166
170
  badgeText: onNavigate ? undefined : badgeText,
167
171
  badgeColor: statusColor,
168
172
  customBadge: customBadge,
169
- bottomRightText: timeLabel,
173
+ bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
174
+ timestamp: event.timestamp
175
+ }),
170
176
  showChevron: true,
171
177
  expandedContent: expandedContent,
172
178
  isExpanded: isExpanded,
173
179
  expandedGlowColor: statusColor,
174
- onPress: onPress
180
+ onPress: () => onPress(index)
175
181
  });
176
- }
182
+ });
177
183
  const styles = StyleSheet.create({
178
184
  badgeContainer: {
179
185
  flexDirection: "row",
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { useCallback, useState } from "react";
13
- import { View, Text, ScrollView, StyleSheet } from "react-native";
13
+ import { View, Text, FlatList, StyleSheet } from "react-native";
14
14
  import { Navigation, buoyColors } from "@buoy-gg/shared-ui";
15
15
  import { RouteEventItemCompact } from "./RouteEventItemCompact";
16
16
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -38,16 +38,40 @@ export function RouteEventsTimeline({
38
38
  })]
39
39
  });
40
40
  }
41
- return /*#__PURE__*/_jsx(ScrollView, {
42
- contentContainerStyle: styles.listContent,
43
- children: events.map((event, index) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
44
- event: event,
45
- visitNumber: visitCounts.get(index) || 1,
46
- isExpanded: expandedIndex === index,
47
- onPress: () => handleItemPress(index),
48
- onNavigate: onNavigate
49
- }, `event-${index}-${event.timestamp}`))
50
- });
41
+ return (
42
+ /*#__PURE__*/
43
+ // Virtualized — the previous ScrollView + map mounted ALL events (at
44
+ // 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.
47
+ _jsx(FlatList, {
48
+ contentContainerStyle: styles.listContent,
49
+ data: events
50
+ // STABLE content keys — index-based keys remount every row when a new
51
+ // event prepends (measured 4 JS FPS during automation, which emits
52
+ // route events on every bounce; tool-bench 2026-07-06).
53
+ ,
54
+ keyExtractor: event => `${event.timestamp}-${event.pathname}`,
55
+ extraData: expandedIndex,
56
+ renderItem: ({
57
+ item: event,
58
+ index
59
+ }) => /*#__PURE__*/_jsx(RouteEventItemCompact, {
60
+ event: event,
61
+ visitNumber: visitCounts.get(index) || 1,
62
+ isExpanded: expandedIndex === index,
63
+ index: index,
64
+ onPress: handleItemPress,
65
+ onNavigate: onNavigate
66
+ }),
67
+ showsVerticalScrollIndicator: true,
68
+ removeClippedSubviews: true,
69
+ initialNumToRender: 12,
70
+ maxToRenderPerBatch: 8,
71
+ windowSize: 5,
72
+ updateCellsBatchingPeriod: 50
73
+ })
74
+ );
51
75
  }
52
76
  const styles = StyleSheet.create({
53
77
  listContent: {
@@ -48,19 +48,28 @@ export function useRouteEvents(options = {}) {
48
48
  setIsListening(false);
49
49
  return;
50
50
  }
51
+
52
+ // Frame-coalesced (see network's useNetworkEventList): navigation
53
+ // bursts land as one commit per frame instead of one per emission.
54
+ let rafId = null;
55
+ let pending = null;
51
56
  const unsubscribe = routeEventStore.subscribeToEvents(allEvents => {
52
- // Limit events
53
- if (allEvents.length > maxEvents) {
54
- setEvents(allEvents.slice(0, maxEvents));
55
- } else {
56
- setEvents(allEvents);
57
- }
57
+ pending = allEvents;
58
+ if (rafId != null) return;
59
+ rafId = requestAnimationFrame(() => {
60
+ rafId = null;
61
+ if (!pending) return;
62
+ const latest = pending;
63
+ pending = null;
64
+ setEvents(latest.length > maxEvents ? latest.slice(0, maxEvents) : latest);
65
+ });
58
66
  });
59
67
 
60
68
  // Mark as listening after successful subscription
61
69
  setIsListening(true);
62
70
  return () => {
63
71
  unsubscribe();
72
+ if (rafId != null) cancelAnimationFrame(rafId);
64
73
  // Note: Don't set isListening=false here because we might be
65
74
  // re-subscribing due to dependency change, not actually stopping
66
75
  };
@@ -8,8 +8,11 @@ export interface RouteEventItemCompactProps {
8
8
  event: RouteChangeEvent;
9
9
  visitNumber: number;
10
10
  isExpanded: boolean;
11
- onPress: () => void;
11
+ /** Row's list index — passed back through onPress so the handler can
12
+ * stay a single stable callback (memo-compatible). */
13
+ index: number;
14
+ onPress: (index: number) => void;
12
15
  onNavigate?: (pathname: string) => void;
13
16
  }
14
- export declare function RouteEventItemCompact({ event, visitNumber, isExpanded, onPress, onNavigate, }: RouteEventItemCompactProps): import("react").JSX.Element;
17
+ export declare const RouteEventItemCompact: import("react").NamedExoticComponent<RouteEventItemCompactProps>;
15
18
  //# sourceMappingURL=RouteEventItemCompact.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA6GD,wBAAgB,qBAAqB,CAAC,EACpC,KAAK,EACL,WAAW,EACX,UAAU,EACV,OAAO,EACP,UAAU,GACX,EAAE,0BAA0B,+BA6D5B"}
1
+ {"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;0DACsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAgHD,eAAO,MAAM,qBAAqB,kEAmEhC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,GACX,EAAE,wBAAwB,+BAiC1B"}
1
+ {"version":3,"file":"RouteEventsTimeline.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsTimeline.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,UAAU,wBAAwB;IAChC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,WAAW,EACX,UAAU,GACX,EAAE,wBAAwB,+BAoD1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CA4CtB"}
1
+ {"version":3,"file":"useRouteEvents.d.ts","sourceRoot":"","sources":["../../../src/hooks/useRouteEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAEL,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,uBAAuB;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,cAAc,CAC5B,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAoDtB"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/route-events",
3
- "version": "5.0.0",
4
- "description": "route-events package",
3
+ "version": "6.0.1",
4
+ "description": "Navigation devtool for React Native — live stack, full sitemap, jump to any screen. Expo Router & React Navigation. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
7
7
  "types": "lib/typescript/index.d.ts",
@@ -26,11 +26,11 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/floating-tools-core": "5.0.0",
30
- "@buoy-gg/shared-ui": "5.0.0"
29
+ "@buoy-gg/floating-tools-core": "6.0.1",
30
+ "@buoy-gg/shared-ui": "6.0.1"
31
31
  },
32
32
  "peerDependencies": {
33
- "@buoy-gg/license": "5.0.0",
33
+ "@buoy-gg/license": "6.0.1",
34
34
  "@react-native-async-storage/async-storage": "^2.0.0 || ^3.1.0",
35
35
  "@react-navigation/native": "*",
36
36
  "expo-router": "*",
@@ -49,7 +49,7 @@
49
49
  "@types/react-native": "^0.73.0",
50
50
  "expo-router": "~5.0.7",
51
51
  "typescript": "~5.8.3",
52
- "@buoy-gg/license": "5.0.0"
52
+ "@buoy-gg/license": "6.0.1"
53
53
  },
54
54
  "react-native-builder-bob": {
55
55
  "source": "src",