@buoy-gg/network 7.0.13 → 7.0.15

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 (123) hide show
  1. package/lib/commonjs/index.js +39 -0
  2. package/lib/commonjs/network/components/NetworkEventDetailView.js +30 -3
  3. package/lib/commonjs/network/components/NetworkEventItemCompact.js +4 -0
  4. package/lib/commonjs/network/components/NetworkHeaderLive.js +26 -6
  5. package/lib/commonjs/network/components/NetworkHeaderMenu.js +183 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +126 -5
  7. package/lib/commonjs/network/components/NetworkModal.js +136 -29
  8. package/lib/commonjs/network/components/NetworkNumberPrompt.js +178 -0
  9. package/lib/commonjs/network/components/NetworkOverrideEditor.js +797 -0
  10. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.js +102 -0
  11. package/lib/commonjs/network/components/NetworkOverridesButton.js +88 -0
  12. package/lib/commonjs/network/components/NetworkOverridesScreen.js +489 -0
  13. package/lib/commonjs/network/components/response-editor/Explorer.js +973 -0
  14. package/lib/commonjs/network/components/response-editor/deleteNestedDataByPath.js +56 -0
  15. package/lib/commonjs/network/components/response-editor/editorContext.js +33 -0
  16. package/lib/commonjs/network/components/response-editor/updateNestedDataByPath.js +61 -0
  17. package/lib/commonjs/network/hooks/useOverrideRules.js +36 -0
  18. package/lib/commonjs/network/overrides/applyToFetch.js +80 -0
  19. package/lib/commonjs/network/overrides/applyToXhr.js +190 -0
  20. package/lib/commonjs/network/overrides/engine.js +70 -0
  21. package/lib/commonjs/network/overrides/matchRule.js +81 -0
  22. package/lib/commonjs/network/overrides/overrideRulesStore.js +550 -0
  23. package/lib/commonjs/network/overrides/presets.js +70 -0
  24. package/lib/commonjs/network/overrides/resolveOverride.js +158 -0
  25. package/lib/commonjs/network/overrides/types.js +85 -0
  26. package/lib/commonjs/network/sync/networkSyncAdapter.js +230 -1
  27. package/lib/commonjs/network/types/index.js +5 -1
  28. package/lib/commonjs/network/utils/networkEventStore.js +52 -6
  29. package/lib/commonjs/network/utils/networkListener.js +90 -10
  30. package/lib/commonjs/preset.js +11 -0
  31. package/lib/module/index.js +15 -0
  32. package/lib/module/network/components/NetworkEventDetailView.js +31 -4
  33. package/lib/module/network/components/NetworkEventItemCompact.js +5 -1
  34. package/lib/module/network/components/NetworkHeaderLive.js +26 -6
  35. package/lib/module/network/components/NetworkHeaderMenu.js +177 -0
  36. package/lib/module/network/components/NetworkListScreen.js +127 -6
  37. package/lib/module/network/components/NetworkModal.js +138 -31
  38. package/lib/module/network/components/NetworkNumberPrompt.js +174 -0
  39. package/lib/module/network/components/NetworkOverrideEditor.js +794 -0
  40. package/lib/module/network/components/NetworkOverrideHeaderButton.js +99 -0
  41. package/lib/module/network/components/NetworkOverridesButton.js +85 -0
  42. package/lib/module/network/components/NetworkOverridesScreen.js +485 -0
  43. package/lib/module/network/components/response-editor/Explorer.js +972 -0
  44. package/lib/module/network/components/response-editor/deleteNestedDataByPath.js +51 -0
  45. package/lib/module/network/components/response-editor/editorContext.js +29 -0
  46. package/lib/module/network/components/response-editor/updateNestedDataByPath.js +56 -0
  47. package/lib/module/network/hooks/useOverrideRules.js +32 -0
  48. package/lib/module/network/overrides/applyToFetch.js +72 -0
  49. package/lib/module/network/overrides/applyToXhr.js +185 -0
  50. package/lib/module/network/overrides/engine.js +64 -0
  51. package/lib/module/network/overrides/matchRule.js +75 -0
  52. package/lib/module/network/overrides/overrideRulesStore.js +547 -0
  53. package/lib/module/network/overrides/presets.js +65 -0
  54. package/lib/module/network/overrides/resolveOverride.js +152 -0
  55. package/lib/module/network/overrides/types.js +80 -0
  56. package/lib/module/network/sync/networkSyncAdapter.js +230 -1
  57. package/lib/module/network/types/index.js +3 -1
  58. package/lib/module/network/utils/networkEventStore.js +52 -6
  59. package/lib/module/network/utils/networkListener.js +91 -10
  60. package/lib/module/preset.js +13 -1
  61. package/lib/typescript/index.d.ts +8 -0
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  64. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +6 -0
  65. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  66. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +8 -1
  67. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  68. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts +46 -0
  69. package/lib/typescript/network/components/NetworkHeaderMenu.d.ts.map +1 -0
  70. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  71. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  72. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts +33 -0
  73. package/lib/typescript/network/components/NetworkNumberPrompt.d.ts.map +1 -0
  74. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts +17 -0
  75. package/lib/typescript/network/components/NetworkOverrideEditor.d.ts.map +1 -0
  76. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts +27 -0
  77. package/lib/typescript/network/components/NetworkOverrideHeaderButton.d.ts.map +1 -0
  78. package/lib/typescript/network/components/NetworkOverridesButton.d.ts +17 -0
  79. package/lib/typescript/network/components/NetworkOverridesButton.d.ts.map +1 -0
  80. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +29 -0
  81. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -0
  82. package/lib/typescript/network/components/response-editor/Explorer.d.ts +17 -0
  83. package/lib/typescript/network/components/response-editor/Explorer.d.ts.map +1 -0
  84. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts +9 -0
  85. package/lib/typescript/network/components/response-editor/deleteNestedDataByPath.d.ts.map +1 -0
  86. package/lib/typescript/network/components/response-editor/editorContext.d.ts +29 -0
  87. package/lib/typescript/network/components/response-editor/editorContext.d.ts.map +1 -0
  88. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts +10 -0
  89. package/lib/typescript/network/components/response-editor/updateNestedDataByPath.d.ts.map +1 -0
  90. package/lib/typescript/network/hooks/useOverrideRules.d.ts +29 -0
  91. package/lib/typescript/network/hooks/useOverrideRules.d.ts.map +1 -0
  92. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts +16 -0
  93. package/lib/typescript/network/overrides/__tests__/applyToFetch.test.d.ts.map +1 -0
  94. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts +19 -0
  95. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -0
  96. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts +22 -0
  97. package/lib/typescript/network/overrides/__tests__/resolveOverride.test.d.ts.map +1 -0
  98. package/lib/typescript/network/overrides/applyToFetch.d.ts +50 -0
  99. package/lib/typescript/network/overrides/applyToFetch.d.ts.map +1 -0
  100. package/lib/typescript/network/overrides/applyToXhr.d.ts +43 -0
  101. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -0
  102. package/lib/typescript/network/overrides/engine.d.ts +47 -0
  103. package/lib/typescript/network/overrides/engine.d.ts.map +1 -0
  104. package/lib/typescript/network/overrides/matchRule.d.ts +22 -0
  105. package/lib/typescript/network/overrides/matchRule.d.ts.map +1 -0
  106. package/lib/typescript/network/overrides/overrideRulesStore.d.ts +205 -0
  107. package/lib/typescript/network/overrides/overrideRulesStore.d.ts.map +1 -0
  108. package/lib/typescript/network/overrides/presets.d.ts +38 -0
  109. package/lib/typescript/network/overrides/presets.d.ts.map +1 -0
  110. package/lib/typescript/network/overrides/resolveOverride.d.ts +46 -0
  111. package/lib/typescript/network/overrides/resolveOverride.d.ts.map +1 -0
  112. package/lib/typescript/network/overrides/types.d.ts +169 -0
  113. package/lib/typescript/network/overrides/types.d.ts.map +1 -0
  114. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +73 -0
  115. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  116. package/lib/typescript/network/types/index.d.ts +10 -0
  117. package/lib/typescript/network/types/index.d.ts.map +1 -1
  118. package/lib/typescript/network/utils/networkEventStore.d.ts +14 -0
  119. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  120. package/lib/typescript/network/utils/networkListener.d.ts +3 -0
  121. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  122. package/lib/typescript/preset.d.ts.map +1 -1
  123. package/package.json +2 -2
@@ -4,11 +4,29 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.stopNetworkListener = exports.startNetworkListener = exports.removeAllNetworkListeners = exports.networkListener = exports.isNetworkListening = exports.getNetworkListenerCount = exports.addNetworkListener = void 0;
7
+ var _engine = require("../overrides/engine");
8
+ var _applyToXhr = require("../overrides/applyToXhr");
9
+ var _applyToFetch = require("../overrides/applyToFetch");
10
+ var _types = require("../overrides/types");
7
11
  /**
8
12
  * Network listener using Reactotron-style event pattern
9
13
  * Simple and reliable network interception for React Native
10
14
  */
11
15
 
16
+ /**
17
+ * True only while `originalFetch` is being CALLED (not awaited).
18
+ *
19
+ * Overrides hook both `fetch` and `XMLHttpRequest`, because Expo's native
20
+ * `expo/fetch` bypasses XHR while bare RN's whatwg-fetch polyfill runs straight
21
+ * through it. On bare RN that means one `fetch()` would otherwise be overridden
22
+ * twice — a doubled delay and a doubled hit count.
23
+ *
24
+ * whatwg-fetch creates and sends its XHR synchronously inside the Promise
25
+ * executor, so a flag raised across the call expression alone is enough to
26
+ * cover the inner `send()` without leaking across the await.
27
+ */
28
+ let insideOverriddenFetch = false;
29
+
12
30
  // Extended XMLHttpRequest interface for monkey-patching
13
31
 
14
32
  // Event types for network operations
@@ -203,7 +221,7 @@ class NetworkListener {
203
221
  * Handle XMLHttpRequest response processing
204
222
  * This method processes the response and emits appropriate events
205
223
  */
206
- handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr") {
224
+ handleXHRResponse(xhr, requestId, cleanUrl, method, requestHeaders, requestData, params, startTime, isError = false, clientType = "xhr", override) {
207
225
  const duration = startTime ? Date.now() - startTime : 0;
208
226
 
209
227
  // XHR's duration was ALREADY total (measured at readyState 4 / load), so
@@ -232,7 +250,8 @@ class NetworkListener {
232
250
  },
233
251
  error: {
234
252
  message: isError ? "Request failed" : "Network error or request aborted"
235
- }
253
+ },
254
+ override
236
255
  });
237
256
  return;
238
257
  }
@@ -371,7 +390,8 @@ class NetworkListener {
371
390
  headers: responseHeaders,
372
391
  body,
373
392
  size: responseSize
374
- }
393
+ },
394
+ override
375
395
  });
376
396
  } else {
377
397
  this.emit({
@@ -396,7 +416,8 @@ class NetworkListener {
396
416
  },
397
417
  error: {
398
418
  message: `HTTP ${xhr.status}: ${xhr.statusText}`
399
- }
419
+ },
420
+ override
400
421
  });
401
422
  }
402
423
  }
@@ -467,6 +488,18 @@ class NetworkListener {
467
488
  // Determine client type from X-Request-Client header or default to fetch
468
489
  const clientType = requestHeaders["X-Request-Client"] || requestHeaders["x-request-client"] || "fetch";
469
490
 
491
+ /**
492
+ * Response overrides, fetch side.
493
+ *
494
+ * Needed even though `send` below also resolves them: Expo installs a
495
+ * NATIVE `expo/fetch` as `globalThis.fetch`, which never constructs an
496
+ * XMLHttpRequest, so the XHR hook alone would never see these requests.
497
+ * Skipped when `canSynthesizeResponse()` is false so a runtime without a
498
+ * `Response` global degrades to a real request rather than a broken one.
499
+ */
500
+ const outcome = (0, _applyToFetch.canSynthesizeResponse)() ? (0, _engine.overrideForRequest)(url, method) : null;
501
+ const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
502
+
470
503
  // Emit request event
471
504
  self.emit({
472
505
  type: "request",
@@ -479,10 +512,28 @@ class NetworkListener {
479
512
  data: requestData,
480
513
  params: params || undefined,
481
514
  client: clientType
482
- }
515
+ },
516
+ override
483
517
  });
484
518
  try {
485
- const response = await self.originalFetch(input, init);
519
+ if (outcome) await (0, _applyToFetch.sleep)(outcome.delayMs);
520
+ if (outcome?.kind === "fail") throw (0, _applyToFetch.synthesizeFetchError)(outcome);
521
+ let response;
522
+ if (outcome?.kind === "respond") {
523
+ response = (0, _applyToFetch.synthesizeResponse)(url, outcome);
524
+ } else {
525
+ // The flag spans the CALL only — see `insideOverriddenFetch`. It has
526
+ // to be cleared before the await, or it would still be raised for
527
+ // every unrelated request that starts while this one is in flight.
528
+ insideOverriddenFetch = outcome != null;
529
+ let pending;
530
+ try {
531
+ pending = self.originalFetch(input, init);
532
+ } finally {
533
+ insideOverriddenFetch = false;
534
+ }
535
+ response = await pending;
536
+ }
486
537
  // A fetch promise resolves when the HEADERS arrive, not the body — so
487
538
  // this is time-to-first-byte, and it is where `duration` used to stop.
488
539
  // That silently excluded download time, making an identical request
@@ -551,7 +602,8 @@ class NetworkListener {
551
602
  headers: responseHeaders,
552
603
  body,
553
604
  size: responseSize
554
- }
605
+ },
606
+ override
555
607
  });
556
608
  return response;
557
609
  } catch (error) {
@@ -574,7 +626,8 @@ class NetworkListener {
574
626
  error: {
575
627
  message: error instanceof Error ? error.message : "Network error",
576
628
  stack: error instanceof Error ? error.stack : undefined
577
- }
629
+ },
630
+ override
578
631
  });
579
632
  throw error;
580
633
  }
@@ -641,6 +694,22 @@ class NetworkListener {
641
694
  const looksLikeAxios = acceptHeader.includes("application/json") && acceptHeader.includes("text/plain") && acceptHeader.includes("*/*");
642
695
  const clientType = explicitClient || (looksLikeAxios ? "axios" : "xhr");
643
696
 
697
+ /**
698
+ * Response overrides.
699
+ *
700
+ * This is the layer that covers `axios` and raw XMLHttpRequest. It also
701
+ * covers `fetch` on BARE React Native, where the whatwg-fetch polyfill
702
+ * runs through XHR — which is why the fetch wrapper raises
703
+ * `insideOverriddenFetch` around its pass-through, so one request can't
704
+ * be overridden twice.
705
+ *
706
+ * Matching uses the raw `url` (query string included), which is what the
707
+ * developer sees in the list and writes into a rule. No-ops in production
708
+ * builds; see `overrideForRequest`.
709
+ */
710
+ const outcome = insideOverriddenFetch ? null : (0, _engine.overrideForRequest)(url, method);
711
+ const override = outcome ? (0, _types.markForOutcome)(outcome) : undefined;
712
+
644
713
  // Emit request event
645
714
  self.emit({
646
715
  type: "request",
@@ -653,7 +722,8 @@ class NetworkListener {
653
722
  data: requestData,
654
723
  params: params || undefined,
655
724
  client: clientType
656
- }
725
+ },
726
+ override
657
727
  });
658
728
 
659
729
  // Track if we've already processed this request to avoid duplicate events
@@ -661,7 +731,7 @@ class NetworkListener {
661
731
  const processResponse = (isError = false) => {
662
732
  if (processed) return;
663
733
  processed = true;
664
- self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType);
734
+ self.handleXHRResponse(this, requestId || "unknown", cleanUrl, method, requestHeaders, requestData, params, startTime || 0, isError, clientType, override);
665
735
  // Clean up event listeners after processing to prevent memory leaks
666
736
  cleanup();
667
737
  };
@@ -708,6 +778,16 @@ class NetworkListener {
708
778
  this.addEventListener("error", errorListener);
709
779
  this.addEventListener("abort", abortListener);
710
780
  this.addEventListener("readystatechange", readyStateListener);
781
+ if (outcome) {
782
+ // Listeners are attached above FIRST: `runOverride` drives the same
783
+ // readyState transitions a real response would, and the interceptor has
784
+ // to see them (readyState 2 is where TTFB is stamped).
785
+ const handled = (0, _applyToXhr.runOverride)(this, url, outcome, () => self.originalXHRSend.call(this, data));
786
+ // `handled` is false only where this isn't React Native's own
787
+ // XMLHttpRequest and can't be driven by hand. Fall through and send for
788
+ // real rather than dropping the request on the floor.
789
+ if (handled) return;
790
+ }
711
791
  return self.originalXHRSend.call(this, data);
712
792
  };
713
793
  this.isListening = true;
@@ -7,6 +7,7 @@ exports.createNetworkTool = createNetworkTool;
7
7
  exports.networkToolPreset = void 0;
8
8
  var _sharedUi = require("@buoy-gg/shared-ui");
9
9
  var _NetworkModal = require("./network/components/NetworkModal");
10
+ var _overrideRulesStore = require("./network/overrides/overrideRulesStore");
10
11
  var _jsxRuntime = require("react/jsx-runtime");
11
12
  /**
12
13
  * Pre-configured network monitoring tool for FloatingDevTools
@@ -25,6 +26,16 @@ var _jsxRuntime = require("react/jsx-runtime");
25
26
  * ```
26
27
  */
27
28
 
29
+ /**
30
+ * Load persisted response-override rules as soon as the tool is registered.
31
+ *
32
+ * Deliberately at import time rather than when the Network modal mounts: an
33
+ * override has to fire from app boot, with the tool closed and nothing
34
+ * capturing — that is the whole point of persisting rules ("force this endpoint
35
+ * to 500, then restart and watch the boot path"). Hydrating also re-installs
36
+ * the interceptor if any rule is still active. One storage read, dev-only.
37
+ */void _overrideRulesStore.overrideRulesStore.ensureLoaded();
38
+
28
39
  /**
29
40
  * Pre-configured network monitoring tool for FloatingDevTools.
30
41
  * Includes:
@@ -56,6 +56,21 @@ export { formatBytes, formatDuration, formatHttpStatus } from "./network/utils/f
56
56
  // TYPES (For TypeScript users)
57
57
  // =============================================================================
58
58
 
59
+ // =============================================================================
60
+ // RESPONSE OVERRIDES
61
+ // Force a status, body, failure or delay on matching requests — Buoy's answer
62
+ // to Chrome DevTools' Local Overrides, plus the status/latency/failure control
63
+ // Chrome doesn't have.
64
+ // =============================================================================
65
+ export { overrideRulesStore
66
+
67
+ /**
68
+ * @internal — the desktop dashboard puts the rule store in remote-mirror mode
69
+ * so edits made there are forwarded to the device instead of stored locally.
70
+ */ } from "./network/overrides/overrideRulesStore";
71
+ export { draftFromEvent } from "./network/overrides/presets";
72
+ export { MAX_RULES as MAX_OVERRIDE_RULES, FREE_TIER_RULE_LIMIT as FREE_TIER_OVERRIDE_LIMIT, STATUS_MIN as OVERRIDE_STATUS_MIN, STATUS_MAX as OVERRIDE_STATUS_MAX } from "./network/overrides/types";
73
+
59
74
  // =============================================================================
60
75
  // SHARED IGNORED-PATTERN FILTERING
61
76
  // Re-exported from @buoy-gg/shared-ui so consumers wiring the Network detail
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useState, useEffect, useMemo, memo } from "react";
4
4
  import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from "react-native";
5
- import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
5
+ import { Clock, Upload, Download, AlertCircle, ChevronDown, ChevronUp, Lock, Unlock, FileJson, Filter, Globe, Link, Copy, FlaskConical, InlineCopyButton, macOSColors, useFeatureGate, UpgradeModal } from "@buoy-gg/shared-ui";
6
6
  import { formatBytes, formatDuration, formatHttpStatus } from "../utils/formatting";
7
7
  import { formatRelativeTime } from "@buoy-gg/shared-ui";
8
8
  import { DataViewer } from "@buoy-gg/shared-ui/dataViewer";
@@ -73,8 +73,11 @@ const TimingPhases = ({
73
73
  children: /*#__PURE__*/_jsx(View, {
74
74
  style: [styles.phaseBar, {
75
75
  backgroundColor: phase.color,
76
- // A floor so a 1ms phase is still a visible tick.
77
- width: `${Math.max(1, phase.value / span * 100)}%`
76
+ // A floor so a 1ms phase is still a visible tick, and a
77
+ // ceiling because `total` and the phases are measured at
78
+ // different points — ttfb can exceed event.duration, which
79
+ // would push the bar past its track and over the label.
80
+ width: `${Math.min(100, Math.max(1, phase.value / span * 100))}%`
78
81
  }]
79
82
  })
80
83
  }), /*#__PURE__*/_jsx(Text, {
@@ -442,6 +445,15 @@ ${JSON.stringify(requestDetails.responseData, null, 2)}
442
445
  style: styles.httpDurationText,
443
446
  children: formatDuration(event.duration)
444
447
  })]
448
+ }) : null, event.override ? /*#__PURE__*/_jsxs(View, {
449
+ style: styles.overrideBadge,
450
+ children: [/*#__PURE__*/_jsx(FlaskConical, {
451
+ size: 10,
452
+ color: macOSColors.semantic.warning
453
+ }), /*#__PURE__*/_jsx(Text, {
454
+ style: styles.overrideBadgeText,
455
+ children: event.override.ruleName || "Overridden"
456
+ })]
445
457
  }) : null, isPro ? /*#__PURE__*/_jsx(InlineCopyButton
446
458
  // LAZY, not `getFullRequestDetails()`. CopyButton resolves a
447
459
  // function value at press time; calling it here instead
@@ -795,6 +807,20 @@ const styles = StyleSheet.create({
795
807
  color: macOSColors.text.secondary,
796
808
  fontSize: 11
797
809
  },
810
+ overrideBadge: {
811
+ flexDirection: "row",
812
+ alignItems: "center",
813
+ gap: 4,
814
+ paddingHorizontal: 6,
815
+ paddingVertical: 3,
816
+ borderRadius: 4,
817
+ backgroundColor: macOSColors.semantic.warningBackground
818
+ },
819
+ overrideBadgeText: {
820
+ color: macOSColors.semantic.warning,
821
+ fontSize: 10,
822
+ fontWeight: "700"
823
+ },
798
824
  copyFullButton: {
799
825
  marginLeft: "auto",
800
826
  flexShrink: 0,
@@ -947,7 +973,8 @@ const styles = StyleSheet.create({
947
973
  height: 7,
948
974
  borderRadius: 4,
949
975
  backgroundColor: macOSColors.background.hover,
950
- justifyContent: "center"
976
+ justifyContent: "center",
977
+ overflow: "hidden"
951
978
  },
952
979
  phaseBar: {
953
980
  height: 7,
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { memo } from "react";
4
4
  import { StyleSheet, View, Text } from "react-native";
5
- import { ChevronRight, Upload, Download, Clock, AlertCircle, Bookmark, Pin, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
5
+ import { ChevronRight, Upload, Download, Clock, AlertCircle, Bookmark, FlaskConical, Pin, DevToolsCard, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
6
6
  import { formatBytes, formatDuration } from "../utils/formatting";
7
7
  import { formatRelativeTime } from "@buoy-gg/shared-ui";
8
8
  import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
@@ -170,6 +170,7 @@ function NetworkEventItemCompact({
170
170
  onPress,
171
171
  pinned = false,
172
172
  saved = false,
173
+ overridden = false,
173
174
  onLongPress
174
175
  }) {
175
176
  const statusColor = getStatusColor(event.status, event.error);
@@ -208,6 +209,9 @@ function NetworkEventItemCompact({
208
209
  }) : null, saved ? /*#__PURE__*/_jsx(Bookmark, {
209
210
  size: 10,
210
211
  color: macOSColors.semantic.debug
212
+ }) : null, overridden || event.override ? /*#__PURE__*/_jsx(FlaskConical, {
213
+ size: 10,
214
+ color: macOSColors.semantic.warning
211
215
  }) : null, /*#__PURE__*/_jsx(StatusIndicator, {
212
216
  event: event,
213
217
  isPending: isPending,
@@ -214,7 +214,8 @@ export function NetworkHeaderCopyButton({
214
214
  ignoredPatterns,
215
215
  copySettings,
216
216
  paused,
217
- onUpgradePress
217
+ onUpgradePress,
218
+ label
218
219
  }) {
219
220
  const hasEvents = useNetworkHasMatches({
220
221
  filter,
@@ -222,15 +223,18 @@ export function NetworkHeaderCopyButton({
222
223
  paused
223
224
  });
224
225
  if (!isPro) {
225
- return /*#__PURE__*/_jsx(TouchableOpacity, {
226
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
226
227
  "sentry-label": "ignore copy pro",
227
228
  onPress: onUpgradePress,
228
- style: [styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
229
+ style: [label ? styles.copyMenuRow : styles.headerActionButton, !hasEvents && styles.headerActionButtonDisabled],
229
230
  disabled: !hasEvents,
230
- children: /*#__PURE__*/_jsx(Copy, {
231
+ children: [/*#__PURE__*/_jsx(Copy, {
231
232
  size: 14,
232
233
  color: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled
233
- })
234
+ }), label ? /*#__PURE__*/_jsx(Text, {
235
+ style: styles.copyMenuLabel,
236
+ children: label
237
+ }) : null]
234
238
  });
235
239
  }
236
240
  return /*#__PURE__*/_jsx(CopyButton
@@ -239,7 +243,9 @@ export function NetworkHeaderCopyButton({
239
243
  , {
240
244
  value: () => generateNetworkCopyText(filterNetworkEvents(applyIgnoredPatterns(networkEventStore.getEvents(), ignoredPatterns), filter), copySettings),
241
245
  size: 14,
242
- buttonStyle: styles.headerActionButton,
246
+ label: label,
247
+ buttonStyle: label ? styles.copyMenuRow : styles.headerActionButton,
248
+ labelStyle: styles.copyMenuLabel,
243
249
  colors: {
244
250
  idle: hasEvents ? macOSColors.text.secondary : macOSColors.text.disabled,
245
251
  success: macOSColors.semantic.success,
@@ -249,6 +255,20 @@ export function NetworkHeaderCopyButton({
249
255
  });
250
256
  }
251
257
  const styles = StyleSheet.create({
258
+ /** The overflow-menu variant: a full-width row, matching the other items. */
259
+ copyMenuRow: {
260
+ flexDirection: "row",
261
+ alignItems: "center",
262
+ gap: 10,
263
+ paddingHorizontal: 6,
264
+ paddingVertical: 11,
265
+ justifyContent: "flex-start"
266
+ },
267
+ copyMenuLabel: {
268
+ fontSize: 13,
269
+ fontWeight: "600",
270
+ color: macOSColors.text.primary
271
+ },
252
272
  headerChipRow: {
253
273
  flexDirection: "row",
254
274
  alignItems: "center",
@@ -0,0 +1,177 @@
1
+ "use strict";
2
+
3
+ import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
4
+ import { absoluteFill, macOSColors } from "@buoy-gg/shared-ui";
5
+
6
+ /**
7
+ * The header's overflow menu.
8
+ *
9
+ * The toolbar had grown to three status badges and six icon buttons, which on a
10
+ * 402pt screen meant the pending badge was being overlapped by the search
11
+ * button. Something had to leave, and the rule for deciding what is:
12
+ *
13
+ * **A control that COMMUNICATES STATE stays visible. A destination or a rare
14
+ * action goes in the menu.**
15
+ *
16
+ * That keeps the power toggle (are we capturing?), the overrides flask (is the
17
+ * app being lied to, and how many rules?), the status badges (what happened)
18
+ * and Clear (the reproduce loop is clear → repro → read, and burying the first
19
+ * step doubles it). Search stays because it's the only way through 500 rows.
20
+ *
21
+ * Filters, Saved and Copy leave. Filters is the interesting one — it does show
22
+ * state, so the ⋯ carries a dot when a filter is applied rather than hiding
23
+ * that fact.
24
+ */
25
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
26
+ export function NetworkHeaderMenuButton({
27
+ onPress,
28
+ isOpen,
29
+ hasIndicator
30
+ }) {
31
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
32
+ onPress: onPress,
33
+ "sentry-label": "ignore toggle header menu",
34
+ accessibilityLabel: isOpen ? "Close menu" : "More actions",
35
+ style: [styles.button, isOpen && styles.buttonOpen],
36
+ children: [/*#__PURE__*/_jsxs(View, {
37
+ style: styles.dots,
38
+ children: [/*#__PURE__*/_jsx(View, {
39
+ style: [styles.dot, isOpen && styles.dotOpen]
40
+ }), /*#__PURE__*/_jsx(View, {
41
+ style: [styles.dot, isOpen && styles.dotOpen]
42
+ }), /*#__PURE__*/_jsx(View, {
43
+ style: [styles.dot, isOpen && styles.dotOpen]
44
+ })]
45
+ }), hasIndicator ? /*#__PURE__*/_jsx(View, {
46
+ style: styles.indicator
47
+ }) : null]
48
+ });
49
+ }
50
+ /**
51
+ * The dropdown itself, rendered in the modal BODY rather than the header —
52
+ * the header is a fixed-height row and anything overflowing it is clipped, so
53
+ * a panel anchored to the top-right of the body lands exactly where a dropdown
54
+ * from the ⋯ should appear.
55
+ */
56
+ export function NetworkHeaderMenu({
57
+ onClose,
58
+ children
59
+ }) {
60
+ return /*#__PURE__*/_jsxs(View, {
61
+ style: styles.overlay,
62
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
63
+ style: styles.backdrop,
64
+ activeOpacity: 1,
65
+ onPress: onClose,
66
+ "sentry-label": "ignore dismiss header menu"
67
+ }), /*#__PURE__*/_jsx(View, {
68
+ style: styles.panel,
69
+ children: children
70
+ })]
71
+ });
72
+ }
73
+ export function NetworkHeaderMenuItem({
74
+ icon,
75
+ label,
76
+ detail,
77
+ onPress
78
+ }) {
79
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
80
+ onPress: onPress,
81
+ "sentry-label": "ignore header menu item",
82
+ style: styles.item,
83
+ children: [/*#__PURE__*/_jsx(View, {
84
+ style: styles.itemIcon,
85
+ children: icon
86
+ }), /*#__PURE__*/_jsx(Text, {
87
+ style: styles.itemLabel,
88
+ children: label
89
+ }), detail ? /*#__PURE__*/_jsx(Text, {
90
+ style: styles.itemDetail,
91
+ children: detail
92
+ }) : null]
93
+ });
94
+ }
95
+ const styles = StyleSheet.create({
96
+ // The same 32×32 box every other header action uses, minus the fill and
97
+ // border. Padding alone gave a 3pt-wide hit area AND a shorter box than its
98
+ // neighbours, which is why the dots sat high in the row; a fixed square
99
+ // centres the glyph on the same baseline as the search/power/clear icons.
100
+ button: {
101
+ width: 32,
102
+ height: 32,
103
+ borderRadius: 8,
104
+ alignItems: "center",
105
+ justifyContent: "center"
106
+ },
107
+ // Only lit while the menu is down — the button is the one thing not covered
108
+ // by the backdrop, so it has to look like the thing that is currently open.
109
+ buttonOpen: {
110
+ backgroundColor: macOSColors.background.hover
111
+ },
112
+ dots: {
113
+ flexDirection: "column",
114
+ gap: 2.5,
115
+ alignItems: "center"
116
+ },
117
+ dot: {
118
+ width: 3,
119
+ height: 3,
120
+ borderRadius: 2,
121
+ backgroundColor: macOSColors.text.secondary
122
+ },
123
+ dotOpen: {
124
+ backgroundColor: macOSColors.text.primary
125
+ },
126
+ indicator: {
127
+ position: "absolute",
128
+ // Sits just up-and-right of the top dot. Anchored to the 32pt box's corner
129
+ // it would float off on its own, since the dot column is only 3pt wide.
130
+ top: 6,
131
+ right: 8,
132
+ width: 6,
133
+ height: 6,
134
+ borderRadius: 3,
135
+ backgroundColor: macOSColors.semantic.info
136
+ },
137
+ overlay: {
138
+ ...absoluteFill,
139
+ zIndex: 50
140
+ },
141
+ backdrop: {
142
+ ...absoluteFill,
143
+ backgroundColor: "rgba(0,0,0,0.5)"
144
+ },
145
+ panel: {
146
+ position: "absolute",
147
+ top: 6,
148
+ right: 8,
149
+ minWidth: 196,
150
+ paddingVertical: 4,
151
+ borderRadius: 8,
152
+ borderWidth: 1,
153
+ borderColor: macOSColors.border.hover,
154
+ backgroundColor: macOSColors.background.card
155
+ },
156
+ item: {
157
+ flexDirection: "row",
158
+ alignItems: "center",
159
+ gap: 10,
160
+ paddingHorizontal: 12,
161
+ paddingVertical: 11
162
+ },
163
+ itemIcon: {
164
+ width: 16,
165
+ alignItems: "center"
166
+ },
167
+ itemLabel: {
168
+ flex: 1,
169
+ fontSize: 13,
170
+ fontWeight: "600",
171
+ color: macOSColors.text.primary
172
+ },
173
+ itemDetail: {
174
+ fontSize: 11,
175
+ color: macOSColors.text.muted
176
+ }
177
+ });