@buoy-gg/network 7.0.10 → 7.0.12

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
@@ -40,6 +40,7 @@ Want a different label or color? Build the tool with `createNetworkTool({ name,
40
40
  - **Full request detail** — request/response headers, payloads, HTTP status, and duration for every call, with status-colored rows.
41
41
  - **Filter and search** — narrow by status code, HTTP method, or URL pattern; search captured events; pause/resume capture; clear history.
42
42
  - **Pin the one that failed** — pin a request to hold it at the top of the list (ignoring your filters), or save it to a separate favorites list. Both keep a full snapshot, so it survives Clear, the 500-request cap, and an app restart. Long-press a row, or use the buttons in the request detail header.
43
+ - **Step between requests** — the detail view has a Previous/Next footer that walks exactly what the list was showing (same pins, filters, and search — or your Saved list), so comparing two calls doesn't mean going back and finding your place.
43
44
  - **Ignore the noise** — a shared ignored-domains/URL filter (`useIgnoredPatterns`) hides endpoints you never want to see, shared with the Events tool.
44
45
  - **Hooks for custom UIs** — `useNetworkEvents`, `useNetworkEventList`, `useNetworkBadgeCounts`, and `useNetworkSaved` expose the capture stream with narrow per-consumer subscriptions.
45
46
 
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkDetailStepper = void 0;
7
+ var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
+ var _useNetworkEventList = require("../hooks/useNetworkEventList");
10
+ var _useNetworkSaved = require("../hooks/useNetworkSaved");
11
+ var _withPinnedFirst = require("../utils/withPinnedFirst");
12
+ var _selectSavedEvents = require("../utils/selectSavedEvents");
13
+ var _jsxRuntime = require("react/jsx-runtime");
14
+ /**
15
+ * Previous/Next footer for the request DETAIL screen — step through requests
16
+ * without bouncing back to the list.
17
+ *
18
+ * It is its OWN leaf (rule3) because it is the only part of the detail screen
19
+ * that needs the event LIST. Subscribing here keeps the request stream out of
20
+ * the detail body's render path, which is the expensive subtree (headers,
21
+ * bodies, JSON viewers, diff views): a request arriving re-renders this
22
+ * ~30-node footer and nothing else.
23
+ *
24
+ * The order it steps through is EXACTLY what the list screen renders —
25
+ * `[...pinned, ...rest]` off the same `useNetworkEventList` + `splitPinned`
26
+ * derivation — so "Next" always lands on the row below the one the user
27
+ * tapped. Because the live filter is an input, typing in the search box or
28
+ * flipping a status chip re-scopes the stepper in place; the counter is
29
+ * "request N of <what the list is showing>", never "N of everything ever
30
+ * captured". Detail opened from the Saved screen steps through the SAVED
31
+ * list instead (with its own search applied), for the same reason.
32
+ */
33
+
34
+ const NetworkDetailStepper = exports.NetworkDetailStepper = /*#__PURE__*/(0, _react.memo)(function NetworkDetailStepper({
35
+ eventId,
36
+ paused,
37
+ filter,
38
+ ignoredPatterns,
39
+ isPro,
40
+ fromSaved,
41
+ savedSearch,
42
+ onSelect,
43
+ applySafeAreaInset
44
+ }) {
45
+ const {
46
+ visibleEvents
47
+ } = (0, _useNetworkEventList.useNetworkEventList)({
48
+ filter,
49
+ ignoredPatterns,
50
+ paused,
51
+ isPro
52
+ });
53
+ const {
54
+ pinnedEvents,
55
+ pinnedLiveIds,
56
+ savedRecords
57
+ } = (0, _useNetworkSaved.useNetworkSaved)();
58
+ const ordered = (0, _react.useMemo)(() => {
59
+ if (fromSaved) {
60
+ return (0, _selectSavedEvents.selectSavedEvents)(savedRecords, isPro, savedSearch).events;
61
+ }
62
+ // Same split the list screen does — pins render above the FlatList, so
63
+ // they come first here too.
64
+ const {
65
+ pinned,
66
+ rest
67
+ } = (0, _withPinnedFirst.splitPinned)(visibleEvents, pinnedEvents, pinnedLiveIds);
68
+ return pinned.length === 0 ? rest : [...pinned, ...rest];
69
+ }, [fromSaved, savedRecords, savedSearch, isPro, visibleEvents, pinnedEvents, pinnedLiveIds]);
70
+ const index = ordered.findIndex(event => event.id === eventId);
71
+
72
+ // Not in the list at all: the filter changed under it while it was open, it
73
+ // aged past the 500-request cap, or it is a saved snapshot with no live twin.
74
+ // There is no coherent "next" in that state, so show no footer rather than a
75
+ // counter that lies.
76
+ if (index < 0) return null;
77
+
78
+ // Both lists are newest-first, so "Previous" walks toward the NEWER request —
79
+ // the same direction as scrolling up, which is what the arrow means here.
80
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.EventStepperFooter, {
81
+ currentIndex: index,
82
+ totalItems: ordered.length,
83
+ itemLabel: "Request",
84
+ applySafeAreaInset: applySafeAreaInset,
85
+ onPrevious: () => {
86
+ const target = ordered[index - 1];
87
+ if (target) onSelect(target.id);
88
+ },
89
+ onNext: () => {
90
+ const target = ordered[index + 1];
91
+ if (target) onSelect(target.id);
92
+ }
93
+ });
94
+ });
@@ -15,6 +15,7 @@ var _NetworkCopySettingsView = require("./NetworkCopySettingsView");
15
15
  var _NetworkHeaderLive = require("./NetworkHeaderLive");
16
16
  var _NetworkListScreen = require("./NetworkListScreen");
17
17
  var _NetworkSavedScreen = require("./NetworkSavedScreen");
18
+ var _NetworkDetailStepper = require("./NetworkDetailStepper");
18
19
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
19
20
  var _useLiveNetworkEvent = require("../hooks/useLiveNetworkEvent");
20
21
  var _networkSavedStore = require("../utils/networkSavedStore");
@@ -31,7 +32,14 @@ function NetworkDetailScreen({
31
32
  eventId,
32
33
  paused,
33
34
  ignoredPatternValues,
34
- onTogglePattern
35
+ onTogglePattern,
36
+ isPro,
37
+ filter,
38
+ ignoredPatterns,
39
+ fromSaved,
40
+ savedSearch,
41
+ onSelectEvent,
42
+ applySafeAreaInset
35
43
  }) {
36
44
  // Resolve the ID to the live event so a request opened while pending picks
37
45
  // up its response/error in place instead of freezing on the snapshot taken
@@ -40,10 +48,26 @@ function NetworkDetailScreen({
40
48
  paused: paused || !visible
41
49
  });
42
50
  if (!visible || !event) return null;
43
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
44
- event: event,
45
- ignoredPatterns: ignoredPatternValues,
46
- onTogglePattern: onTogglePattern
51
+ // Column: the detail body scrolls, the stepper stays pinned under it. The
52
+ // body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
53
+ // wrapper the footer would scroll away with the content.
54
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
55
+ style: styles.detailRoot,
56
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
57
+ event: event,
58
+ ignoredPatterns: ignoredPatternValues,
59
+ onTogglePattern: onTogglePattern
60
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkDetailStepper.NetworkDetailStepper, {
61
+ eventId: event.id,
62
+ paused: paused,
63
+ filter: filter,
64
+ ignoredPatterns: ignoredPatterns,
65
+ isPro: isPro,
66
+ fromSaved: fromSaved,
67
+ savedSearch: savedSearch,
68
+ onSelect: onSelectEvent,
69
+ applySafeAreaInset: applySafeAreaInset
70
+ })]
47
71
  });
48
72
  }
49
73
  function NetworkFilterScreen({
@@ -144,6 +168,10 @@ function NetworkModalInner({
144
168
  const [activeTab, setActiveTab] = (0, _react.useState)("filters");
145
169
  const [searchText, setSearchText] = (0, _react.useState)("");
146
170
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
171
+ // The Saved screen's search box lives HERE, not in that screen, because the
172
+ // detail footer steps through the same list the user was just looking at —
173
+ // and the Saved screen is unmounted while a detail is open.
174
+ const [savedSearch, setSavedSearch] = (0, _react.useState)("");
147
175
  const searchInputRef = (0, _react.useRef)(null);
148
176
  // Ignored domains/URL patterns live in a shared, persisted singleton store
149
177
  // (@buoy-gg/shared-ui) so the Network tool and the Events tool stay in sync.
@@ -228,8 +256,13 @@ function NetworkModalInner({
228
256
  status: prev.status === status ? undefined : status
229
257
  }));
230
258
  };
231
- const handleModeChange = _mode => {
232
- // Mode changes handled by JsModal
259
+
260
+ // JsModal owns the mode; the only thing this modal needs it for is the
261
+ // detail footer's bottom inset — a bottom sheet sits on the home indicator,
262
+ // a floating window does not (same call StorageModalWithTabs makes).
263
+ const [modalMode, setModalMode] = (0, _react.useState)("bottomSheet");
264
+ const handleModeChange = mode => {
265
+ setModalMode(mode);
233
266
  };
234
267
  (0, _react.useEffect)(() => {
235
268
  if (isSearchActive) {
@@ -386,8 +419,10 @@ function NetworkModalInner({
386
419
  // it — inside JsModal's ScrollView wrapper every row mounts, which at
387
420
  // the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
388
421
  // NetworkListScreen). The Saved screen is a FlatList too, so it takes the
389
- // same deal. Detail/filter/copy still use the wrapper.
390
- const listOwnsScroll = selectedEventId == null && !showFilterView;
422
+ // same deal. The DETAIL screen owns it as well, so its prev/next footer
423
+ // stays pinned instead of scrolling away with the body. Filter/copy still
424
+ // use the wrapper.
425
+ const bodyOwnsScroll = selectedEventId != null || !showFilterView;
391
426
  const body = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
392
427
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
393
428
  style: styles.container,
@@ -396,7 +431,19 @@ function NetworkModalInner({
396
431
  eventId: selectedEventId,
397
432
  paused: paused,
398
433
  ignoredPatternValues: ignoredPatternValues,
399
- onTogglePattern: toggleIgnoredPattern
434
+ onTogglePattern: toggleIgnoredPattern,
435
+ isPro: isPro,
436
+ filter: filter,
437
+ ignoredPatterns: ignoredPatterns
438
+ // `showSavedView` is what Back returns to, so it is also which list
439
+ // the user came from — and therefore which one to step through.
440
+ ,
441
+ fromSaved: showSavedView,
442
+ savedSearch: savedSearch,
443
+ onSelectEvent: setSelectedEventId
444
+ // Inline (desktop panel) has no home indicator to clear.
445
+ ,
446
+ applySafeAreaInset: !inline && modalMode === "bottomSheet"
400
447
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
401
448
  visible: showFilterView && activeTab === "filters" && !selectedEventId,
402
449
  paused: paused,
@@ -419,6 +466,8 @@ function NetworkModalInner({
419
466
  visible: showSavedView && selectedEventId == null && !showFilterView,
420
467
  isPro: isPro,
421
468
  copySettings: copySettings,
469
+ search: savedSearch,
470
+ onSearchChange: setSavedSearch,
422
471
  onEventPress: handleEventPress,
423
472
  onUpgradePress: () => showUpgrade("saved")
424
473
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
@@ -464,7 +513,7 @@ function NetworkModalInner({
464
513
  enablePersistence: true,
465
514
  initialMode: "bottomSheet",
466
515
  enableGlitchEffects: true,
467
- disableScrollWrapper: listOwnsScroll,
516
+ disableScrollWrapper: bodyOwnsScroll,
468
517
  styles: {},
469
518
  children: body
470
519
  });
@@ -474,6 +523,12 @@ const styles = _reactNative.StyleSheet.create({
474
523
  flex: 1,
475
524
  backgroundColor: _sharedUi.macOSColors.background.base
476
525
  },
526
+ /** Detail = scrolling body + pinned stepper footer. `minHeight: 0` so the
527
+ * body's ScrollView scrolls instead of stretching the column. */
528
+ detailRoot: {
529
+ flex: 1,
530
+ minHeight: 0
531
+ },
477
532
  /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
478
533
  * FlatList can scroll instead of stretching the column. */
479
534
  inlineRoot: {
@@ -10,7 +10,7 @@ var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _NetworkEventItemCompact = require("./NetworkEventItemCompact");
11
11
  var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
12
12
  var _generateNetworkCopyText = require("../utils/generateNetworkCopyText");
13
- var _filterNetworkEvents = require("../utils/filterNetworkEvents");
13
+ var _selectSavedEvents = require("../utils/selectSavedEvents");
14
14
  var _useNetworkSaved = require("../hooks/useNetworkSaved");
15
15
  var _networkSavedStore = require("../utils/networkSavedStore");
16
16
  var _jsxRuntime = require("react/jsx-runtime");
@@ -51,6 +51,8 @@ function NetworkSavedScreen({
51
51
  visible,
52
52
  isPro,
53
53
  copySettings,
54
+ search: searchProp,
55
+ onSearchChange,
54
56
  onEventPress,
55
57
  onUpgradePress
56
58
  }) {
@@ -60,22 +62,17 @@ function NetworkSavedScreen({
60
62
  toggleSave,
61
63
  clearSaved
62
64
  } = (0, _useNetworkSaved.useNetworkSaved)();
63
- const [search, setSearch] = (0, _react.useState)("");
65
+ const [searchState, setSearchState] = (0, _react.useState)("");
66
+ const controlled = searchProp != null && onSearchChange != null;
67
+ const search = controlled ? searchProp : searchState;
68
+ const setSearch = controlled ? onSearchChange : setSearchState;
64
69
  if (!visible) return null;
65
70
 
66
- /**
67
- * A downgrade (or a limit change) can leave a free user holding more saves
68
- * than the free tier allows. Rather than deleting their data, the overflow
69
- * is LOCKED — oldest first, so the newest saves stay reachable — and the
70
- * banner offers the upgrade that unlocks it.
71
- */
72
- const limit = isPro ? _networkSavedStore.MAX_SAVED : _networkSavedStore.FREE_TIER_SAVED_LIMIT;
73
- const unlocked = savedRecords.slice(0, limit);
74
- const lockedCount = savedRecords.length - unlocked.length;
75
- const events = unlocked.map(record => record.event);
76
- const filtered = search ? (0, _filterNetworkEvents.filterNetworkEvents)(events, {
77
- searchText: search
78
- }) : events;
71
+ // Shared with the detail footer, so "Next" can't disagree with the row below.
72
+ const {
73
+ events: filtered,
74
+ lockedCount
75
+ } = (0, _selectSavedEvents.selectSavedEvents)(savedRecords, isPro, search);
79
76
  const handleLongPress = event => {
80
77
  toggleSave(event);
81
78
  };
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.selectSavedEvents = selectSavedEvents;
7
+ var _filterNetworkEvents = require("./filterNetworkEvents");
8
+ var _networkSavedStore = require("./networkSavedStore");
9
+ /**
10
+ * The SAVED screen's visible list, as a pure function.
11
+ *
12
+ * Two places need the exact same list: the screen that renders it
13
+ * (`NetworkSavedScreen`) and the detail footer that steps through it
14
+ * (`NetworkDetailStepper`). If they derived it independently, "Next" would
15
+ * eventually disagree with the row below — the same drift `withPinnedFirst`
16
+ * exists to prevent for the live list.
17
+ */
18
+
19
+ function selectSavedEvents(savedRecords, isPro, search) {
20
+ /**
21
+ * A downgrade (or a limit change) can leave a free user holding more saves
22
+ * than the free tier allows. Rather than deleting their data, the overflow
23
+ * is LOCKED — oldest first, so the newest saves stay reachable.
24
+ */
25
+ const limit = isPro ? _networkSavedStore.MAX_SAVED : _networkSavedStore.FREE_TIER_SAVED_LIMIT;
26
+ const unlocked = savedRecords.slice(0, limit);
27
+ const events = unlocked.map(record => record.event);
28
+ return {
29
+ events: search ? (0, _filterNetworkEvents.filterNetworkEvents)(events, {
30
+ searchText: search
31
+ }) : events,
32
+ lockedCount: savedRecords.length - unlocked.length
33
+ };
34
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Previous/Next footer for the request DETAIL screen — step through requests
5
+ * without bouncing back to the list.
6
+ *
7
+ * It is its OWN leaf (rule3) because it is the only part of the detail screen
8
+ * that needs the event LIST. Subscribing here keeps the request stream out of
9
+ * the detail body's render path, which is the expensive subtree (headers,
10
+ * bodies, JSON viewers, diff views): a request arriving re-renders this
11
+ * ~30-node footer and nothing else.
12
+ *
13
+ * The order it steps through is EXACTLY what the list screen renders —
14
+ * `[...pinned, ...rest]` off the same `useNetworkEventList` + `splitPinned`
15
+ * derivation — so "Next" always lands on the row below the one the user
16
+ * tapped. Because the live filter is an input, typing in the search box or
17
+ * flipping a status chip re-scopes the stepper in place; the counter is
18
+ * "request N of <what the list is showing>", never "N of everything ever
19
+ * captured". Detail opened from the Saved screen steps through the SAVED
20
+ * list instead (with its own search applied), for the same reason.
21
+ */
22
+
23
+ import { memo, useMemo } from "react";
24
+ import { EventStepperFooter } from "@buoy-gg/shared-ui";
25
+ import { useNetworkEventList } from "../hooks/useNetworkEventList";
26
+ import { useNetworkSaved } from "../hooks/useNetworkSaved";
27
+ import { splitPinned } from "../utils/withPinnedFirst";
28
+ import { selectSavedEvents } from "../utils/selectSavedEvents";
29
+ import { jsx as _jsx } from "react/jsx-runtime";
30
+ export const NetworkDetailStepper = /*#__PURE__*/memo(function NetworkDetailStepper({
31
+ eventId,
32
+ paused,
33
+ filter,
34
+ ignoredPatterns,
35
+ isPro,
36
+ fromSaved,
37
+ savedSearch,
38
+ onSelect,
39
+ applySafeAreaInset
40
+ }) {
41
+ const {
42
+ visibleEvents
43
+ } = useNetworkEventList({
44
+ filter,
45
+ ignoredPatterns,
46
+ paused,
47
+ isPro
48
+ });
49
+ const {
50
+ pinnedEvents,
51
+ pinnedLiveIds,
52
+ savedRecords
53
+ } = useNetworkSaved();
54
+ const ordered = useMemo(() => {
55
+ if (fromSaved) {
56
+ return selectSavedEvents(savedRecords, isPro, savedSearch).events;
57
+ }
58
+ // Same split the list screen does — pins render above the FlatList, so
59
+ // they come first here too.
60
+ const {
61
+ pinned,
62
+ rest
63
+ } = splitPinned(visibleEvents, pinnedEvents, pinnedLiveIds);
64
+ return pinned.length === 0 ? rest : [...pinned, ...rest];
65
+ }, [fromSaved, savedRecords, savedSearch, isPro, visibleEvents, pinnedEvents, pinnedLiveIds]);
66
+ const index = ordered.findIndex(event => event.id === eventId);
67
+
68
+ // Not in the list at all: the filter changed under it while it was open, it
69
+ // aged past the 500-request cap, or it is a saved snapshot with no live twin.
70
+ // There is no coherent "next" in that state, so show no footer rather than a
71
+ // counter that lies.
72
+ if (index < 0) return null;
73
+
74
+ // Both lists are newest-first, so "Previous" walks toward the NEWER request —
75
+ // the same direction as scrolling up, which is what the arrow means here.
76
+ return /*#__PURE__*/_jsx(EventStepperFooter, {
77
+ currentIndex: index,
78
+ totalItems: ordered.length,
79
+ itemLabel: "Request",
80
+ applySafeAreaInset: applySafeAreaInset,
81
+ onPrevious: () => {
82
+ const target = ordered[index - 1];
83
+ if (target) onSelect(target.id);
84
+ },
85
+ onNext: () => {
86
+ const target = ordered[index + 1];
87
+ if (target) onSelect(target.id);
88
+ }
89
+ });
90
+ });
@@ -10,6 +10,7 @@ import { NetworkCopySettingsView, DEFAULT_COPY_SETTINGS } from "./NetworkCopySet
10
10
  import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton, NetworkSavedButton } from "./NetworkHeaderLive";
11
11
  import { NetworkListScreen } from "./NetworkListScreen";
12
12
  import { NetworkSavedScreen } from "./NetworkSavedScreen";
13
+ import { NetworkDetailStepper } from "./NetworkDetailStepper";
13
14
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
14
15
  import { useLiveNetworkEvent } from "../hooks/useLiveNetworkEvent";
15
16
  import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
@@ -26,7 +27,14 @@ function NetworkDetailScreen({
26
27
  eventId,
27
28
  paused,
28
29
  ignoredPatternValues,
29
- onTogglePattern
30
+ onTogglePattern,
31
+ isPro,
32
+ filter,
33
+ ignoredPatterns,
34
+ fromSaved,
35
+ savedSearch,
36
+ onSelectEvent,
37
+ applySafeAreaInset
30
38
  }) {
31
39
  // Resolve the ID to the live event so a request opened while pending picks
32
40
  // up its response/error in place instead of freezing on the snapshot taken
@@ -35,10 +43,26 @@ function NetworkDetailScreen({
35
43
  paused: paused || !visible
36
44
  });
37
45
  if (!visible || !event) return null;
38
- return /*#__PURE__*/_jsx(NetworkEventDetailView, {
39
- event: event,
40
- ignoredPatterns: ignoredPatternValues,
41
- onTogglePattern: onTogglePattern
46
+ // Column: the detail body scrolls, the stepper stays pinned under it. The
47
+ // body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
48
+ // wrapper the footer would scroll away with the content.
49
+ return /*#__PURE__*/_jsxs(View, {
50
+ style: styles.detailRoot,
51
+ children: [/*#__PURE__*/_jsx(NetworkEventDetailView, {
52
+ event: event,
53
+ ignoredPatterns: ignoredPatternValues,
54
+ onTogglePattern: onTogglePattern
55
+ }), /*#__PURE__*/_jsx(NetworkDetailStepper, {
56
+ eventId: event.id,
57
+ paused: paused,
58
+ filter: filter,
59
+ ignoredPatterns: ignoredPatterns,
60
+ isPro: isPro,
61
+ fromSaved: fromSaved,
62
+ savedSearch: savedSearch,
63
+ onSelect: onSelectEvent,
64
+ applySafeAreaInset: applySafeAreaInset
65
+ })]
42
66
  });
43
67
  }
44
68
  function NetworkFilterScreen({
@@ -139,6 +163,10 @@ function NetworkModalInner({
139
163
  const [activeTab, setActiveTab] = useState("filters");
140
164
  const [searchText, setSearchText] = useState("");
141
165
  const [isSearchActive, setIsSearchActive] = useState(false);
166
+ // The Saved screen's search box lives HERE, not in that screen, because the
167
+ // detail footer steps through the same list the user was just looking at —
168
+ // and the Saved screen is unmounted while a detail is open.
169
+ const [savedSearch, setSavedSearch] = useState("");
142
170
  const searchInputRef = useRef(null);
143
171
  // Ignored domains/URL patterns live in a shared, persisted singleton store
144
172
  // (@buoy-gg/shared-ui) so the Network tool and the Events tool stay in sync.
@@ -223,8 +251,13 @@ function NetworkModalInner({
223
251
  status: prev.status === status ? undefined : status
224
252
  }));
225
253
  };
226
- const handleModeChange = _mode => {
227
- // Mode changes handled by JsModal
254
+
255
+ // JsModal owns the mode; the only thing this modal needs it for is the
256
+ // detail footer's bottom inset — a bottom sheet sits on the home indicator,
257
+ // a floating window does not (same call StorageModalWithTabs makes).
258
+ const [modalMode, setModalMode] = useState("bottomSheet");
259
+ const handleModeChange = mode => {
260
+ setModalMode(mode);
228
261
  };
229
262
  useEffect(() => {
230
263
  if (isSearchActive) {
@@ -381,8 +414,10 @@ function NetworkModalInner({
381
414
  // it — inside JsModal's ScrollView wrapper every row mounts, which at
382
415
  // the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
383
416
  // NetworkListScreen). The Saved screen is a FlatList too, so it takes the
384
- // same deal. Detail/filter/copy still use the wrapper.
385
- const listOwnsScroll = selectedEventId == null && !showFilterView;
417
+ // same deal. The DETAIL screen owns it as well, so its prev/next footer
418
+ // stays pinned instead of scrolling away with the body. Filter/copy still
419
+ // use the wrapper.
420
+ const bodyOwnsScroll = selectedEventId != null || !showFilterView;
386
421
  const body = /*#__PURE__*/_jsxs(_Fragment, {
387
422
  children: [/*#__PURE__*/_jsxs(View, {
388
423
  style: styles.container,
@@ -391,7 +426,19 @@ function NetworkModalInner({
391
426
  eventId: selectedEventId,
392
427
  paused: paused,
393
428
  ignoredPatternValues: ignoredPatternValues,
394
- onTogglePattern: toggleIgnoredPattern
429
+ onTogglePattern: toggleIgnoredPattern,
430
+ isPro: isPro,
431
+ filter: filter,
432
+ ignoredPatterns: ignoredPatterns
433
+ // `showSavedView` is what Back returns to, so it is also which list
434
+ // the user came from — and therefore which one to step through.
435
+ ,
436
+ fromSaved: showSavedView,
437
+ savedSearch: savedSearch,
438
+ onSelectEvent: setSelectedEventId
439
+ // Inline (desktop panel) has no home indicator to clear.
440
+ ,
441
+ applySafeAreaInset: !inline && modalMode === "bottomSheet"
395
442
  }), /*#__PURE__*/_jsx(NetworkFilterScreen, {
396
443
  visible: showFilterView && activeTab === "filters" && !selectedEventId,
397
444
  paused: paused,
@@ -414,6 +461,8 @@ function NetworkModalInner({
414
461
  visible: showSavedView && selectedEventId == null && !showFilterView,
415
462
  isPro: isPro,
416
463
  copySettings: copySettings,
464
+ search: savedSearch,
465
+ onSearchChange: setSavedSearch,
417
466
  onEventPress: handleEventPress,
418
467
  onUpgradePress: () => showUpgrade("saved")
419
468
  }), /*#__PURE__*/_jsx(NetworkListScreen, {
@@ -459,7 +508,7 @@ function NetworkModalInner({
459
508
  enablePersistence: true,
460
509
  initialMode: "bottomSheet",
461
510
  enableGlitchEffects: true,
462
- disableScrollWrapper: listOwnsScroll,
511
+ disableScrollWrapper: bodyOwnsScroll,
463
512
  styles: {},
464
513
  children: body
465
514
  });
@@ -469,6 +518,12 @@ const styles = StyleSheet.create({
469
518
  flex: 1,
470
519
  backgroundColor: macOSColors.background.base
471
520
  },
521
+ /** Detail = scrolling body + pinned stepper footer. `minHeight: 0` so the
522
+ * body's ScrollView scrolls instead of stretching the column. */
523
+ detailRoot: {
524
+ flex: 1,
525
+ minHeight: 0
526
+ },
472
527
  /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
473
528
  * FlatList can scroll instead of stretching the column. */
474
529
  inlineRoot: {
@@ -23,9 +23,9 @@ import { Bookmark, CopyButton, Search, macOSColors } from "@buoy-gg/shared-ui";
23
23
  import { NetworkEventItemCompact } from "./NetworkEventItemCompact";
24
24
  import { NetworkUpgradeBanner } from "./NetworkUpgradeBanner";
25
25
  import { generateNetworkCopyText } from "../utils/generateNetworkCopyText";
26
- import { filterNetworkEvents } from "../utils/filterNetworkEvents";
26
+ import { selectSavedEvents } from "../utils/selectSavedEvents";
27
27
  import { useNetworkSaved } from "../hooks/useNetworkSaved";
28
- import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
28
+ import { FREE_TIER_SAVED_LIMIT } from "../utils/networkSavedStore";
29
29
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
30
30
  const keyExtractor = item => item.id;
31
31
  function EmptyState() {
@@ -47,6 +47,8 @@ export function NetworkSavedScreen({
47
47
  visible,
48
48
  isPro,
49
49
  copySettings,
50
+ search: searchProp,
51
+ onSearchChange,
50
52
  onEventPress,
51
53
  onUpgradePress
52
54
  }) {
@@ -56,22 +58,17 @@ export function NetworkSavedScreen({
56
58
  toggleSave,
57
59
  clearSaved
58
60
  } = useNetworkSaved();
59
- const [search, setSearch] = useState("");
61
+ const [searchState, setSearchState] = useState("");
62
+ const controlled = searchProp != null && onSearchChange != null;
63
+ const search = controlled ? searchProp : searchState;
64
+ const setSearch = controlled ? onSearchChange : setSearchState;
60
65
  if (!visible) return null;
61
66
 
62
- /**
63
- * A downgrade (or a limit change) can leave a free user holding more saves
64
- * than the free tier allows. Rather than deleting their data, the overflow
65
- * is LOCKED — oldest first, so the newest saves stay reachable — and the
66
- * banner offers the upgrade that unlocks it.
67
- */
68
- const limit = isPro ? MAX_SAVED : FREE_TIER_SAVED_LIMIT;
69
- const unlocked = savedRecords.slice(0, limit);
70
- const lockedCount = savedRecords.length - unlocked.length;
71
- const events = unlocked.map(record => record.event);
72
- const filtered = search ? filterNetworkEvents(events, {
73
- searchText: search
74
- }) : events;
67
+ // Shared with the detail footer, so "Next" can't disagree with the row below.
68
+ const {
69
+ events: filtered,
70
+ lockedCount
71
+ } = selectSavedEvents(savedRecords, isPro, search);
75
72
  const handleLongPress = event => {
76
73
  toggleSave(event);
77
74
  };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The SAVED screen's visible list, as a pure function.
5
+ *
6
+ * Two places need the exact same list: the screen that renders it
7
+ * (`NetworkSavedScreen`) and the detail footer that steps through it
8
+ * (`NetworkDetailStepper`). If they derived it independently, "Next" would
9
+ * eventually disagree with the row below — the same drift `withPinnedFirst`
10
+ * exists to prevent for the live list.
11
+ */
12
+
13
+ import { filterNetworkEvents } from "./filterNetworkEvents";
14
+ import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "./networkSavedStore";
15
+ export function selectSavedEvents(savedRecords, isPro, search) {
16
+ /**
17
+ * A downgrade (or a limit change) can leave a free user holding more saves
18
+ * than the free tier allows. Rather than deleting their data, the overflow
19
+ * is LOCKED — oldest first, so the newest saves stay reachable.
20
+ */
21
+ const limit = isPro ? MAX_SAVED : FREE_TIER_SAVED_LIMIT;
22
+ const unlocked = savedRecords.slice(0, limit);
23
+ const events = unlocked.map(record => record.event);
24
+ return {
25
+ events: search ? filterNetworkEvents(events, {
26
+ searchText: search
27
+ }) : events,
28
+ lockedCount: savedRecords.length - unlocked.length
29
+ };
30
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Previous/Next footer for the request DETAIL screen — step through requests
3
+ * without bouncing back to the list.
4
+ *
5
+ * It is its OWN leaf (rule3) because it is the only part of the detail screen
6
+ * that needs the event LIST. Subscribing here keeps the request stream out of
7
+ * the detail body's render path, which is the expensive subtree (headers,
8
+ * bodies, JSON viewers, diff views): a request arriving re-renders this
9
+ * ~30-node footer and nothing else.
10
+ *
11
+ * The order it steps through is EXACTLY what the list screen renders —
12
+ * `[...pinned, ...rest]` off the same `useNetworkEventList` + `splitPinned`
13
+ * derivation — so "Next" always lands on the row below the one the user
14
+ * tapped. Because the live filter is an input, typing in the search box or
15
+ * flipping a status chip re-scopes the stepper in place; the counter is
16
+ * "request N of <what the list is showing>", never "N of everything ever
17
+ * captured". Detail opened from the Saved screen steps through the SAVED
18
+ * list instead (with its own search applied), for the same reason.
19
+ */
20
+ import { type IgnoredPattern } from "@buoy-gg/shared-ui";
21
+ import type { NetworkFilter } from "../types";
22
+ interface NetworkDetailStepperProps {
23
+ /** The request currently on screen. */
24
+ eventId: string;
25
+ paused: boolean;
26
+ filter: NetworkFilter;
27
+ ignoredPatterns: readonly IgnoredPattern[];
28
+ isPro: boolean;
29
+ /** Detail was opened from the Saved screen — step through THAT list. */
30
+ fromSaved: boolean;
31
+ /** The Saved screen's search box (lifted to the modal root so both agree). */
32
+ savedSearch: string;
33
+ onSelect: (eventId: string) => void;
34
+ /** Only a bottom sheet sits on the home indicator — see NetworkModal. */
35
+ applySafeAreaInset: boolean;
36
+ }
37
+ export declare const NetworkDetailStepper: import("react").NamedExoticComponent<NetworkDetailStepperProps>;
38
+ export {};
39
+ //# sourceMappingURL=NetworkDetailStepper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NetworkDetailStepper.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkDetailStepper.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,UAAU,CAAC;AAM5D,UAAU,yBAAyB;IACjC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,eAAe,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,KAAK,EAAE,OAAO,CAAC;IACf,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,yEAAyE;IACzE,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED,eAAO,MAAM,oBAAoB,iEAmE/B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkModal.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkModal.tsx"],"names":[],"mappings":"AAiDA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C;AA0lBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,oBAAyB,+BAa/D"}
1
+ {"version":3,"file":"NetworkModal.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkModal.tsx"],"names":[],"mappings":"AAkDA,UAAU,iBAAiB;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C;AAopBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,+BAMpD;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5C;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,GAAE,oBAAyB,+BAa/D"}
@@ -20,10 +20,21 @@ interface NetworkSavedScreenProps {
20
20
  visible: boolean;
21
21
  isPro: boolean;
22
22
  copySettings: CopySettings;
23
+ /**
24
+ * Controlled search box — pass both halves or neither, same deal as
25
+ * NetworkModal's `paused`.
26
+ *
27
+ * NetworkModal controls it because the detail screen's prev/next footer
28
+ * steps through THIS list, and this screen is unmounted while a detail is
29
+ * open (see NetworkDetailStepper). A host rendering the screen on its own
30
+ * can omit both and keep the local state.
31
+ */
32
+ search?: string;
33
+ onSearchChange?: (search: string) => void;
23
34
  /** Row tap — the root's setSelectedEvent (inherently stable setter). */
24
35
  onEventPress: (event: NetworkEvent) => void;
25
36
  onUpgradePress: () => void;
26
37
  }
27
- export declare function NetworkSavedScreen({ visible, isPro, copySettings, onEventPress, onUpgradePress, }: NetworkSavedScreenProps): import("react").JSX.Element | null;
38
+ export declare function NetworkSavedScreen({ visible, isPro, copySettings, search: searchProp, onSearchChange, onEventPress, onUpgradePress, }: NetworkSavedScreenProps): import("react").JSX.Element | null;
28
39
  export {};
29
40
  //# sourceMappingURL=NetworkSavedScreen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NetworkSavedScreen.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkSavedScreen.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAyB9D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B,wEAAwE;IACxE,YAAY,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,EAAE,uBAAuB,sCAgIzB"}
1
+ {"version":3,"file":"NetworkSavedScreen.d.ts","sourceRoot":"","sources":["../../../../src/network/components/NetworkSavedScreen.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAqB9D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,YAAY,CAAC;IAC3B;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,wEAAwE;IACxE,YAAY,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAAC,EACjC,OAAO,EACP,KAAK,EACL,YAAY,EACZ,MAAM,EAAE,UAAU,EAClB,cAAc,EACd,YAAY,EACZ,cAAc,GACf,EAAE,uBAAuB,sCA2HzB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The SAVED screen's visible list, as a pure function.
3
+ *
4
+ * Two places need the exact same list: the screen that renders it
5
+ * (`NetworkSavedScreen`) and the detail footer that steps through it
6
+ * (`NetworkDetailStepper`). If they derived it independently, "Next" would
7
+ * eventually disagree with the row below — the same drift `withPinnedFirst`
8
+ * exists to prevent for the live list.
9
+ */
10
+ import type { NetworkEvent } from "../types";
11
+ import { type SavedNetworkRecord } from "./networkSavedStore";
12
+ export interface SavedEventSelection {
13
+ /** Unlocked saves, minus anything the search box excludes. */
14
+ events: NetworkEvent[];
15
+ /** Saves past the tier ceiling — rendered as the upgrade banner, not rows. */
16
+ lockedCount: number;
17
+ }
18
+ export declare function selectSavedEvents(savedRecords: readonly SavedNetworkRecord[], isPro: boolean, search: string): SavedEventSelection;
19
+ //# sourceMappingURL=selectSavedEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectSavedEvents.d.ts","sourceRoot":"","sources":["../../../../src/network/utils/selectSavedEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,8EAA8E;IAC9E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,SAAS,kBAAkB,EAAE,EAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,GACb,mBAAmB,CAarB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/network",
3
- "version": "7.0.10",
3
+ "version": "7.0.12",
4
4
  "description": "In-app network inspector for React Native — requests, responses, timing & errors live, with GraphQL operation parsing. Part of Buoy devtools.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "sideEffects": false,
28
28
  "dependencies": {
29
- "@buoy-gg/shared-ui": "^7.0.10"
29
+ "@buoy-gg/shared-ui": "^7.0.12"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "*",