@buoy-gg/network 7.0.12 → 7.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/lib/commonjs/index.js +52 -0
  2. package/lib/commonjs/network/components/NetworkDetailStepper.js +26 -14
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +86 -14
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +32 -7
  6. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  7. package/lib/commonjs/network/components/NetworkListScreen.js +134 -8
  8. package/lib/commonjs/network/components/NetworkModal.js +198 -51
  9. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  10. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  11. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  12. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  13. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  14. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  15. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  16. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  17. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  18. package/lib/commonjs/network/hooks/useNetworkSelection.js +53 -0
  19. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  20. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  21. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  22. package/lib/commonjs/network/overrides/engine.js +70 -0
  23. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  24. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  25. package/lib/commonjs/network/overrides/presets.js +70 -0
  26. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  27. package/lib/commonjs/network/overrides/types.js +85 -0
  28. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  29. package/lib/commonjs/network/types/index.js +5 -1
  30. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  31. package/lib/commonjs/network/utils/networkListener.js +90 -10
  32. package/lib/commonjs/network/utils/networkSelectionStore.js +193 -0
  33. package/lib/commonjs/preset.js +11 -0
  34. package/lib/module/index.js +19 -0
  35. package/lib/module/network/components/NetworkDetailStepper.js +27 -15
  36. package/lib/module/network/components/NetworkEventDetailView.js +87 -15
  37. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  38. package/lib/module/network/components/NetworkHeaderLive.js +32 -7
  39. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  40. package/lib/module/network/components/NetworkListScreen.js +135 -9
  41. package/lib/module/network/components/NetworkModal.js +200 -53
  42. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  43. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  44. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  45. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  46. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  47. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  48. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  49. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  50. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  51. package/lib/module/network/hooks/useNetworkSelection.js +46 -0
  52. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  53. package/lib/module/network/overrides/applyToFetch.js +72 -0
  54. package/lib/module/network/overrides/applyToXhr.js +185 -0
  55. package/lib/module/network/overrides/engine.js +64 -0
  56. package/lib/module/network/overrides/matchRule.js +75 -0
  57. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  58. package/lib/module/network/overrides/presets.js +65 -0
  59. package/lib/module/network/overrides/resolveOverride.js +152 -0
  60. package/lib/module/network/overrides/types.js +80 -0
  61. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  62. package/lib/module/network/types/index.js +3 -1
  63. package/lib/module/network/utils/networkEventStore.js +52 -6
  64. package/lib/module/network/utils/networkListener.js +91 -10
  65. package/lib/module/network/utils/networkSelectionStore.js +188 -0
  66. package/lib/module/preset.js +13 -1
  67. package/lib/typescript/index.d.ts +9 -0
  68. package/lib/typescript/index.d.ts.map +1 -1
  69. package/lib/typescript/network/components/NetworkDetailStepper.d.ts +8 -5
  70. package/lib/typescript/network/components/NetworkDetailStepper.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +36 -1
  72. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  73. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  74. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  75. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +13 -3
  76. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  77. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  78. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  79. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  80. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  81. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  82. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  83. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  84. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  85. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  86. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  87. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  88. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  89. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  90. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  91. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  92. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  93. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  94. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  95. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  96. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  97. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  98. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  99. package/lib/typescript/network/hooks/useNetworkSelection.d.ts +26 -0
  100. package/lib/typescript/network/hooks/useNetworkSelection.d.ts.map +1 -0
  101. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  102. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  103. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  104. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  105. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  106. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  107. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  108. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  109. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  110. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  111. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  112. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  113. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  114. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  115. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  116. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  117. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  118. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  119. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  120. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  121. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  122. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  123. package/lib/typescript/network/overrides/types.d.ts +169 -0
  124. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  125. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  126. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  127. package/lib/typescript/network/types/index.d.ts +10 -0
  128. package/lib/typescript/network/types/index.d.ts.map +1 -1
  129. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  130. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  131. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  132. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  133. package/lib/typescript/network/utils/networkSelectionStore.d.ts +117 -0
  134. package/lib/typescript/network/utils/networkSelectionStore.d.ts.map +1 -0
  135. package/lib/typescript/preset.d.ts.map +1 -1
  136. package/package.json +2 -2
@@ -2,18 +2,25 @@
2
2
 
3
3
  import { useState, useRef, useEffect, useCallback } from "react";
4
4
  import { View, StyleSheet, TouchableOpacity, TextInput } from "react-native";
5
- import { Search, Filter, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, persistentStorage, TabSelector, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns } from "@buoy-gg/shared-ui";
5
+ import { Search, Filter, Bookmark, X, JsModal, ModalHeader, devToolsStorageKeys, macOSColors, persistentStorage, TabSelector, useFeatureGate, UpgradeModal, PowerToggleButton, useIgnoredPatterns } from "@buoy-gg/shared-ui";
6
6
  import { NetworkFilterViewV3 } from "./NetworkFilterViewV3";
7
7
  import { TickProvider } from "../hooks/useTickEveryMinute";
8
8
  import { NetworkEventDetailView } from "./NetworkEventDetailView";
9
9
  import { NetworkCopySettingsView, DEFAULT_COPY_SETTINGS } from "./NetworkCopySettingsView";
10
- import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton, NetworkSavedButton } from "./NetworkHeaderLive";
10
+ import { NetworkHeaderBadges, NetworkClearButton, NetworkDetailPinActions, NetworkHeaderCopyButton } from "./NetworkHeaderLive";
11
11
  import { NetworkListScreen } from "./NetworkListScreen";
12
12
  import { NetworkSavedScreen } from "./NetworkSavedScreen";
13
+ import { NetworkOverridesScreen } from "./NetworkOverridesScreen";
14
+ import { NetworkOverridesButton } from "./NetworkOverridesButton";
15
+ import { NetworkOverrideHeaderButton } from "./NetworkOverrideHeaderButton";
16
+ import { NetworkHeaderMenu, NetworkHeaderMenuButton, NetworkHeaderMenuItem } from "./NetworkHeaderMenu";
13
17
  import { NetworkDetailStepper } from "./NetworkDetailStepper";
14
18
  import { useNetworkEventList } from "../hooks/useNetworkEventList";
15
19
  import { useLiveNetworkEvent } from "../hooks/useLiveNetworkEvent";
20
+ import { useHasNetworkSelection, useNetworkBodyEventId, useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
21
+ import { networkSelectionStore } from "../utils/networkSelectionStore";
16
22
  import { FREE_TIER_SAVED_LIMIT, MAX_SAVED } from "../utils/networkSavedStore";
23
+ import { FREE_TIER_RULE_LIMIT as FREE_TIER_OVERRIDE_LIMIT, MAX_RULES as MAX_OVERRIDE_RULES } from "../overrides/types";
17
24
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
18
25
  // ============================================================================
19
26
  // Screens (rule4 stable router: each gates itself with `return null`; the
@@ -24,7 +31,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
24
31
 
25
32
  function NetworkDetailScreen({
26
33
  visible,
27
- eventId,
28
34
  paused,
29
35
  ignoredPatternValues,
30
36
  onTogglePattern,
@@ -33,34 +39,66 @@ function NetworkDetailScreen({
33
39
  ignoredPatterns,
34
40
  fromSaved,
35
41
  savedSearch,
36
- onSelectEvent,
37
42
  applySafeAreaInset
38
43
  }) {
44
+ /**
45
+ * The detail renders at the IMMEDIATE selection — every step, no delay.
46
+ * Measured, 100 steps at a 150ms tap interval over 64KB payloads:
47
+ *
48
+ * whole detail, bodies included ... 647ms/step
49
+ * detail with no bodies at all .... 43ms/step (over the 150ms tap gap)
50
+ *
51
+ * So ~93% of a step is the request/response BODY trees, and everything else
52
+ * — status, URL breakdown, timing, header counts — is affordable at any tap
53
+ * rate. Deferring the whole detail (which is what this used to do) therefore
54
+ * bought nothing a user could feel: it made the cheap 43ms wait alongside
55
+ * the expensive 450ms instead of splitting them.
56
+ *
57
+ * So only the expensive part waits. `bodyId` trails `selectedId` by the
58
+ * settle window, so `heavyReady` is false exactly while the cursor is still
59
+ * moving — the JSON trees are skipped during a burst and mount once you land.
60
+ */
61
+ const eventId = useSelectedNetworkEventId();
62
+ const settledId = useNetworkBodyEventId();
63
+ const heavyReady = eventId != null && eventId === settledId;
39
64
  // Resolve the ID to the live event so a request opened while pending picks
40
65
  // up its response/error in place instead of freezing on the snapshot taken
41
66
  // when the row was tapped.
42
67
  const event = useLiveNetworkEvent(eventId, {
43
68
  paused: paused || !visible
44
69
  });
45
- if (!visible || !event) return null;
70
+
71
+ // Instrumentation for the benchmark harness. Gated on `heavyReady` on
72
+ // purpose: the cheap chrome now renders on every step, so reporting here
73
+ // unconditionally would make `bodyMs` a duplicate of `cursorMs`. What the
74
+ // harness needs to know is when the expensive TREES landed.
75
+ useEffect(() => {
76
+ if (visible && event && heavyReady) {
77
+ networkSelectionStore.reportRendered(event.id);
78
+ }
79
+ }, [visible, event, heavyReady]);
80
+ if (!visible) return null;
46
81
  // Column: the detail body scrolls, the stepper stays pinned under it. The
47
82
  // body owns its scroll here (see `bodyOwnsScroll`) — inside JsModal's
48
- // wrapper the footer would scroll away with the content.
83
+ // wrapper the footer would scroll away with the content. The spacer keeps
84
+ // the footer at the bottom on the rare frame where the ID resolves to
85
+ // nothing (cleared, aged out, and not saved).
49
86
  return /*#__PURE__*/_jsxs(View, {
50
87
  style: styles.detailRoot,
51
- children: [/*#__PURE__*/_jsx(NetworkEventDetailView, {
88
+ children: [event ? /*#__PURE__*/_jsx(NetworkEventDetailView, {
52
89
  event: event,
53
90
  ignoredPatterns: ignoredPatternValues,
54
- onTogglePattern: onTogglePattern
91
+ onTogglePattern: onTogglePattern,
92
+ showBodies: heavyReady
93
+ }) : /*#__PURE__*/_jsx(View, {
94
+ style: styles.detailBodyFallback
55
95
  }), /*#__PURE__*/_jsx(NetworkDetailStepper, {
56
- eventId: event.id,
57
96
  paused: paused,
58
97
  filter: filter,
59
98
  ignoredPatterns: ignoredPatterns,
60
99
  isPro: isPro,
61
100
  fromSaved: fromSaved,
62
101
  savedSearch: savedSearch,
63
- onSelect: onSelectEvent,
64
102
  applySafeAreaInset: applySafeAreaInset
65
103
  })]
66
104
  });
@@ -155,11 +193,38 @@ function NetworkModalInner({
155
193
  if (controlledPause) onPausedChange(value);else setPausedState(value);
156
194
  };
157
195
  const [filter, setFilter] = useState({});
158
- // The SELECTION is UI state (an ID); the event object it points at is read
159
- // live from the store by the detail screen — see useLiveNetworkEvent.
160
- const [selectedEventId, setSelectedEventId] = useState(null);
196
+ /**
197
+ * The cursor lives in `networkSelectionStore`, and the root subscribes to
198
+ * the BOOLEAN only. That is the whole point: this component re-renders
199
+ * `JsModal` (which can never bail — its `header` prop is a fresh object
200
+ * every render), the entire header, and all five screens. None of them care
201
+ * WHICH request is selected, only THAT one is, and this flips twice a
202
+ * session instead of once per tap.
203
+ */
204
+ const hasSelection = useHasNetworkSelection();
161
205
  const [showFilterView, setShowFilterView] = useState(false);
162
206
  const [showSavedView, setShowSavedView] = useState(false);
207
+ const [showOverridesView, setShowOverridesView] = useState(false);
208
+ const [showHeaderMenu, setShowHeaderMenu] = useState(false);
209
+ /**
210
+ * Overrides are a DEVICE capability — a rule runs inside the app's own
211
+ * `fetch`/`XMLHttpRequest`, which the dashboard doesn't have.
212
+ *
213
+ * That doesn't mean the dashboard can't AUTHOR them: in `inline` mode the
214
+ * rule store runs as a mirror of the connected device (see
215
+ * `overrideRulesStore.enableRemoteMode`), so the screens below are the same
216
+ * ones the phone renders and every edit is forwarded. The one thing the
217
+ * dashboard never does is apply a rule to its own traffic —
218
+ * `disableEngine()` guarantees that.
219
+ */
220
+ const overridesAvailable = true;
221
+ /**
222
+ * A rule prefilled from a request the user was looking at, handed to the
223
+ * Overrides screen when it mounts. Lives here rather than in that screen
224
+ * because the request that seeds it is on a DIFFERENT screen — this is the
225
+ * one place both are in scope.
226
+ */
227
+ const [pendingOverrideDraft, setPendingOverrideDraft] = useState(null);
163
228
  const [activeTab, setActiveTab] = useState("filters");
164
229
  const [searchText, setSearchText] = useState("");
165
230
  const [isSearchActive, setIsSearchActive] = useState(false);
@@ -231,13 +296,32 @@ function NetworkModalInner({
231
296
 
232
297
  // Simple handlers - no useCallback needed per rule2
233
298
  const handleBack = () => {
234
- setSelectedEventId(null);
299
+ networkSelectionStore.clear();
235
300
  };
236
301
 
237
302
  // Stable identity: this is passed down to the memoized list rows.
238
303
  const handleEventPress = useCallback(event => {
239
- setSelectedEventId(event.id);
304
+ networkSelectionStore.select(event.id);
305
+ }, []);
306
+
307
+ /**
308
+ * "Custom…" in a request's Override section. Stable identity because the
309
+ * detail view is memoized on its props and an unstable callback would defeat
310
+ * the bail-out that keeps prev/next stepping cheap.
311
+ */
312
+ const handleOverridePress = useCallback((draft, ruleId) => {
313
+ setPendingOverrideDraft(ruleId ? {
314
+ ...draft,
315
+ id: ruleId
316
+ } : draft);
317
+ setShowOverridesView(true);
318
+ // Leave the detail view, or its header would sit on top of the editor.
319
+ networkSelectionStore.clear();
240
320
  }, []);
321
+
322
+ // Stable identity: the Overrides screen adopts the draft in an effect keyed
323
+ // on both, so an unstable callback would re-run it every render.
324
+ const handlePendingOverrideConsumed = useCallback(() => setPendingOverrideDraft(null), []);
241
325
  const handleSearch = text => {
242
326
  setSearchText(text);
243
327
  setFilter(prev => ({
@@ -294,19 +378,35 @@ function NetworkModalInner({
294
378
  }
295
379
 
296
380
  // Event detail view header
297
- if (selectedEventId) {
381
+ if (hasSelection) {
298
382
  return /*#__PURE__*/_jsxs(ModalHeader, {
299
383
  children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
300
384
  onBack: handleBack
301
385
  }), /*#__PURE__*/_jsx(ModalHeader.Content, {
302
386
  title: "Request Details",
303
387
  centered: true
304
- }), /*#__PURE__*/_jsx(ModalHeader.Actions, {
305
- children: /*#__PURE__*/_jsx(NetworkDetailPinActions, {
306
- eventId: selectedEventId,
388
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
389
+ children: [overridesAvailable ? /*#__PURE__*/_jsx(NetworkOverrideHeaderButton, {
390
+ onOpen: handleOverridePress
391
+ }) : null, /*#__PURE__*/_jsx(NetworkDetailPinActions, {
307
392
  isPro: isPro,
308
393
  onUpgradePress: () => showUpgrade("saved")
309
- })
394
+ })]
395
+ })]
396
+ });
397
+ }
398
+
399
+ // Overrides manager header.
400
+ if (showOverridesView && overridesAvailable) {
401
+ return /*#__PURE__*/_jsxs(ModalHeader, {
402
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
403
+ onBack: () => {
404
+ setShowOverridesView(false);
405
+ setPendingOverrideDraft(null);
406
+ }
407
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
408
+ title: "Response Overrides",
409
+ centered: true
310
410
  })]
311
411
  });
312
412
  }
@@ -378,35 +478,26 @@ function NetworkModalInner({
378
478
  size: 14,
379
479
  color: macOSColors.text.secondary
380
480
  })
381
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
382
- "sentry-label": "ignore filter",
383
- onPress: () => {
384
- setShowFilterView(true);
385
- },
386
- style: [styles.headerActionButton, (filter.status || filter.method || filter.contentType) && styles.activeFilterButton],
387
- children: /*#__PURE__*/_jsx(Filter, {
388
- size: 14,
389
- color: filter.status || filter.method || filter.contentType ? macOSColors.semantic.info : macOSColors.text.muted
390
- })
391
- }), /*#__PURE__*/_jsx(NetworkSavedButton, {
392
- onPress: () => setShowSavedView(true)
393
- }), /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
394
- isPro: isPro,
395
- filter: filter,
396
- ignoredPatterns: ignoredPatterns,
397
- copySettings: copySettings,
398
- paused: paused,
399
- onUpgradePress: () => showUpgrade("export")
400
- }), /*#__PURE__*/_jsx(PowerToggleButton, {
481
+ }), overridesAvailable ? /*#__PURE__*/_jsx(NetworkOverridesButton, {
482
+ onPress: () => setShowOverridesView(true)
483
+ }) : null, /*#__PURE__*/_jsx(PowerToggleButton, {
401
484
  isEnabled: !paused,
402
485
  onToggle: () => setPaused(prev => !prev),
403
486
  accessibilityLabel: "Toggle network interception"
404
487
  }), /*#__PURE__*/_jsx(NetworkClearButton, {
405
488
  paused: paused
489
+ }), /*#__PURE__*/_jsx(NetworkHeaderMenuButton, {
490
+ onPress: () => setShowHeaderMenu(prev => !prev),
491
+ isOpen: showHeaderMenu,
492
+ hasIndicator: hasActiveFilter
406
493
  })]
407
494
  })]
408
495
  });
409
496
  };
497
+
498
+ // Surfaced on the ⋯ so moving Filters into the menu doesn't hide the one
499
+ // thing that button was communicating.
500
+ const hasActiveFilter = !!(filter.status || filter.method || filter.contentType);
410
501
  const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.network.modal();
411
502
  if (!visible) return null;
412
503
 
@@ -417,13 +508,12 @@ function NetworkModalInner({
417
508
  // same deal. The DETAIL screen owns it as well, so its prev/next footer
418
509
  // stays pinned instead of scrolling away with the body. Filter/copy still
419
510
  // use the wrapper.
420
- const bodyOwnsScroll = selectedEventId != null || !showFilterView;
511
+ const bodyOwnsScroll = hasSelection || !showFilterView;
421
512
  const body = /*#__PURE__*/_jsxs(_Fragment, {
422
513
  children: [/*#__PURE__*/_jsxs(View, {
423
514
  style: styles.container,
424
515
  children: [/*#__PURE__*/_jsx(NetworkDetailScreen, {
425
- visible: selectedEventId != null,
426
- eventId: selectedEventId,
516
+ visible: hasSelection,
427
517
  paused: paused,
428
518
  ignoredPatternValues: ignoredPatternValues,
429
519
  onTogglePattern: toggleIgnoredPattern,
@@ -434,13 +524,12 @@ function NetworkModalInner({
434
524
  // the user came from — and therefore which one to step through.
435
525
  ,
436
526
  fromSaved: showSavedView,
437
- savedSearch: savedSearch,
438
- onSelectEvent: setSelectedEventId
527
+ savedSearch: savedSearch
439
528
  // Inline (desktop panel) has no home indicator to clear.
440
529
  ,
441
530
  applySafeAreaInset: !inline && modalMode === "bottomSheet"
442
531
  }), /*#__PURE__*/_jsx(NetworkFilterScreen, {
443
- visible: showFilterView && activeTab === "filters" && !selectedEventId,
532
+ visible: showFilterView && activeTab === "filters" && !hasSelection,
444
533
  paused: paused,
445
534
  isPro: isPro,
446
535
  filter: filter,
@@ -450,15 +539,27 @@ function NetworkModalInner({
450
539
  onAddPattern: addIgnoredPattern,
451
540
  onTogglePatternMode: toggleIgnoredPatternMode
452
541
  }), /*#__PURE__*/_jsx(NetworkCopyScreen, {
453
- visible: showFilterView && activeTab === "copy" && !selectedEventId,
542
+ visible: showFilterView && activeTab === "copy" && !hasSelection,
454
543
  paused: paused,
455
544
  isPro: isPro,
456
545
  filter: filter,
457
546
  ignoredPatterns: ignoredPatterns,
458
547
  settings: copySettings,
459
548
  onSettingsChange: setCopySettings
549
+ }), /*#__PURE__*/_jsx(NetworkOverridesScreen, {
550
+ visible: overridesAvailable && showOverridesView && !hasSelection && !showFilterView,
551
+ isPro: isPro,
552
+ onUpgradePress: () => showUpgrade("overrides"),
553
+ pendingDraft: pendingOverrideDraft,
554
+ onPendingConsumed: handlePendingOverrideConsumed
555
+ // Inline (desktop panel) has no home indicator to clear.
556
+ ,
557
+ applySafeAreaInset: !inline && modalMode === "bottomSheet"
460
558
  }), /*#__PURE__*/_jsx(NetworkSavedScreen, {
461
- visible: showSavedView && selectedEventId == null && !showFilterView,
559
+ visible: showSavedView && !hasSelection && !showFilterView &&
560
+ // Overrides wins in the header branch too — the two must agree, or
561
+ // one screen renders under the other's header.
562
+ !(showOverridesView && overridesAvailable),
462
563
  isPro: isPro,
463
564
  copySettings: copySettings,
464
565
  search: savedSearch,
@@ -466,7 +567,7 @@ function NetworkModalInner({
466
567
  onEventPress: handleEventPress,
467
568
  onUpgradePress: () => showUpgrade("saved")
468
569
  }), /*#__PURE__*/_jsx(NetworkListScreen, {
469
- visible: selectedEventId == null && !showFilterView && !showSavedView,
570
+ visible: !hasSelection && !showFilterView && !showSavedView && !(showOverridesView && overridesAvailable),
470
571
  paused: paused,
471
572
  filter: filter,
472
573
  ignoredPatterns: ignoredPatterns,
@@ -475,12 +576,51 @@ function NetworkModalInner({
475
576
  onEventPress: handleEventPress,
476
577
  onUpgradePress: () => showUpgrade("export")
477
578
  })]
478
- }), /*#__PURE__*/_jsx(UpgradeModal, {
579
+ }), showHeaderMenu ? /*#__PURE__*/_jsxs(NetworkHeaderMenu, {
580
+ onClose: () => setShowHeaderMenu(false),
581
+ children: [/*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
582
+ icon: /*#__PURE__*/_jsx(Filter, {
583
+ size: 14,
584
+ color: hasActiveFilter ? macOSColors.semantic.info : macOSColors.text.secondary
585
+ }),
586
+ label: "Filters",
587
+ detail: hasActiveFilter ? "Active" : undefined,
588
+ onPress: () => {
589
+ setShowHeaderMenu(false);
590
+ setActiveTab("filters");
591
+ setShowFilterView(true);
592
+ }
593
+ }), /*#__PURE__*/_jsx(NetworkHeaderMenuItem, {
594
+ icon: /*#__PURE__*/_jsx(Bookmark, {
595
+ size: 14,
596
+ color: macOSColors.text.secondary
597
+ }),
598
+ label: "Saved requests",
599
+ onPress: () => {
600
+ setShowHeaderMenu(false);
601
+ setShowSavedView(true);
602
+ }
603
+ }), /*#__PURE__*/_jsx(View, {
604
+ style: styles.menuCopyRow,
605
+ children: /*#__PURE__*/_jsx(NetworkHeaderCopyButton, {
606
+ isPro: isPro,
607
+ filter: filter,
608
+ ignoredPatterns: ignoredPatterns,
609
+ copySettings: copySettings,
610
+ paused: paused,
611
+ onUpgradePress: () => {
612
+ setShowHeaderMenu(false);
613
+ showUpgrade("export");
614
+ },
615
+ label: "Copy requests"
616
+ })
617
+ })]
618
+ }) : null, /*#__PURE__*/_jsx(UpgradeModal, {
479
619
  visible: showUpgradeModal,
480
620
  onClose: () => setShowUpgradeModal(false),
481
- featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
482
- description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
483
- benefits: upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
621
+ featureName: upgradeReason === "saved" ? "Saved Requests" : upgradeReason === "overrides" ? "Response Overrides" : "Network Export",
622
+ description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : upgradeReason === "overrides" ? "Force any endpoint to fail, stall or return something else — no backend changes, no code changes. Free keeps one active rule." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
623
+ benefits: upgradeReason === "overrides" ? [`Run ${MAX_OVERRIDE_RULES} rules at once (free keeps ${FREE_TIER_OVERRIDE_LIMIT})`, "Force a 500, a 401 or an empty list in one tap", "Simulate offline, timeouts and slow connections", "Rules survive an app reload, so you can test the boot path"] : upgradeReason === "saved" ? [`Save up to ${MAX_SAVED} requests (free keeps ${FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
484
624
  })]
485
625
  });
486
626
 
@@ -524,6 +664,10 @@ const styles = StyleSheet.create({
524
664
  flex: 1,
525
665
  minHeight: 0
526
666
  },
667
+ /** Holds the footer down when the selected ID resolves to no event. */
668
+ detailBodyFallback: {
669
+ flex: 1
670
+ },
527
671
  /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
528
672
  * FlatList can scroll instead of stretching the column. */
529
673
  inlineRoot: {
@@ -559,6 +703,9 @@ const styles = StyleSheet.create({
559
703
  marginLeft: 6,
560
704
  padding: 4
561
705
  },
706
+ menuCopyRow: {
707
+ paddingHorizontal: 6
708
+ },
562
709
  headerActionButton: {
563
710
  width: 32,
564
711
  height: 32,
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+
3
+ import { useState } from "react";
4
+ import { StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native";
5
+ import { absoluteFill, macOSColors } from "@buoy-gg/shared-ui";
6
+
7
+ /**
8
+ * Entering one number, without the keyboard eating the screen.
9
+ *
10
+ * The custom status / delay / times fields used to be inline inputs sitting
11
+ * mid-form. Focusing one raises the keyboard, which on a phone covers roughly
12
+ * the bottom half of the tool — including, depending on where you'd scrolled,
13
+ * the field you were typing into and the Save button you needed next.
14
+ *
15
+ * Pinning the panel to the TOP is the fix, and it's the same one
16
+ * `StorageValueEditModal` reached for: a centred panel gets shoved up or
17
+ * covered the moment the field focuses, and a bottom sheet is simply
18
+ * underneath it. At the top it stays put and stays readable.
19
+ *
20
+ * Deliberately not a reuse of that component — this asks for a number and
21
+ * nothing else. It has no type inference, no raw-JSON escape hatch and no
22
+ * shape-preservation rules, so borrowing 400 lines to avoid writing 100 would
23
+ * be the wrong trade.
24
+ */
25
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
+ export function NetworkNumberPrompt({
27
+ title,
28
+ hint,
29
+ value,
30
+ placeholder = "Number",
31
+ min,
32
+ max,
33
+ onSubmit,
34
+ onCancel
35
+ }) {
36
+ const [text, setText] = useState(value != null ? String(value) : "");
37
+ const submit = () => {
38
+ const trimmed = text.trim();
39
+ if (!trimmed) {
40
+ onSubmit(null);
41
+ return;
42
+ }
43
+ const parsed = Number(trimmed);
44
+ if (!Number.isFinite(parsed)) {
45
+ onSubmit(null);
46
+ return;
47
+ }
48
+ // Clamped here rather than accepted-then-silently-changed: a field that
49
+ // takes 999 and stores 599 is a field that lies.
50
+ let next = parsed;
51
+ if (min != null) next = Math.max(min, next);
52
+ if (max != null) next = Math.min(max, next);
53
+ onSubmit(next);
54
+ };
55
+ return /*#__PURE__*/_jsxs(View, {
56
+ style: styles.overlay,
57
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
58
+ style: styles.backdrop,
59
+ activeOpacity: 1,
60
+ onPress: onCancel,
61
+ "sentry-label": "ignore dismiss number prompt"
62
+ }), /*#__PURE__*/_jsxs(View, {
63
+ style: styles.panel,
64
+ children: [/*#__PURE__*/_jsx(Text, {
65
+ style: styles.title,
66
+ children: title
67
+ }), /*#__PURE__*/_jsx(TextInput, {
68
+ style: styles.input,
69
+ value: text,
70
+ onChangeText: setText,
71
+ placeholder: placeholder,
72
+ placeholderTextColor: macOSColors.text.muted,
73
+ keyboardType: "number-pad",
74
+ autoFocus: true,
75
+ selectTextOnFocus: true,
76
+ onSubmitEditing: submit,
77
+ returnKeyType: "done",
78
+ "sentry-label": "ignore number prompt input",
79
+ accessibilityLabel: title
80
+ }), hint ? /*#__PURE__*/_jsx(Text, {
81
+ style: styles.hint,
82
+ children: hint
83
+ }) : null, /*#__PURE__*/_jsxs(View, {
84
+ style: styles.actions,
85
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
86
+ onPress: onCancel,
87
+ "sentry-label": "ignore cancel number prompt",
88
+ style: [styles.button, styles.cancelButton],
89
+ children: /*#__PURE__*/_jsx(Text, {
90
+ style: styles.cancelText,
91
+ children: "Cancel"
92
+ })
93
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
94
+ onPress: submit,
95
+ "sentry-label": "ignore save number prompt",
96
+ style: [styles.button, styles.saveButton],
97
+ children: /*#__PURE__*/_jsx(Text, {
98
+ style: styles.saveText,
99
+ children: "Set"
100
+ })
101
+ })]
102
+ })]
103
+ })]
104
+ });
105
+ }
106
+ const styles = StyleSheet.create({
107
+ overlay: {
108
+ ...absoluteFill,
109
+ zIndex: 40
110
+ },
111
+ backdrop: {
112
+ ...absoluteFill,
113
+ backgroundColor: "rgba(0,0,0,0.6)"
114
+ },
115
+ panel: {
116
+ marginHorizontal: 12,
117
+ marginTop: 12,
118
+ padding: 14,
119
+ borderRadius: 10,
120
+ borderWidth: 1,
121
+ borderColor: macOSColors.border.hover,
122
+ backgroundColor: macOSColors.background.card,
123
+ gap: 10
124
+ },
125
+ title: {
126
+ fontSize: 13,
127
+ fontWeight: "700",
128
+ color: macOSColors.text.primary
129
+ },
130
+ input: {
131
+ backgroundColor: macOSColors.background.input,
132
+ borderWidth: 1,
133
+ borderColor: macOSColors.border.input,
134
+ borderRadius: 6,
135
+ paddingHorizontal: 12,
136
+ paddingVertical: 10,
137
+ fontSize: 16,
138
+ fontWeight: "700",
139
+ color: macOSColors.text.primary
140
+ },
141
+ hint: {
142
+ fontSize: 10,
143
+ color: macOSColors.text.muted
144
+ },
145
+ actions: {
146
+ flexDirection: "row",
147
+ gap: 8
148
+ },
149
+ button: {
150
+ flex: 1,
151
+ paddingVertical: 10,
152
+ borderRadius: 6,
153
+ alignItems: "center",
154
+ borderWidth: 1
155
+ },
156
+ cancelButton: {
157
+ borderColor: macOSColors.border.default,
158
+ backgroundColor: macOSColors.background.base
159
+ },
160
+ cancelText: {
161
+ fontSize: 12,
162
+ fontWeight: "600",
163
+ color: macOSColors.text.secondary
164
+ },
165
+ saveButton: {
166
+ borderColor: macOSColors.semantic.info,
167
+ backgroundColor: `${macOSColors.semantic.info}22`
168
+ },
169
+ saveText: {
170
+ fontSize: 12,
171
+ fontWeight: "700",
172
+ color: macOSColors.semantic.info
173
+ }
174
+ });