@buoy-gg/network 7.0.14 → 7.0.16

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 (107) hide show
  1. package/lib/commonjs/index.web.js +395 -0
  2. package/lib/commonjs/network/components/DataViewer.web.js +879 -0
  3. package/lib/commonjs/network/components/NetworkCopyScreen.web.js +48 -0
  4. package/lib/commonjs/network/components/NetworkCopySettingsView.web.js +1242 -0
  5. package/lib/commonjs/network/components/NetworkDetailScreen.web.js +101 -0
  6. package/lib/commonjs/network/components/NetworkDetailStepper.web.js +296 -0
  7. package/lib/commonjs/network/components/NetworkEventDetailView.web.js +1498 -0
  8. package/lib/commonjs/network/components/NetworkEventItemCompact.web.js +802 -0
  9. package/lib/commonjs/network/components/NetworkFilterScreen.web.js +55 -0
  10. package/lib/commonjs/network/components/NetworkFilterViewV3.web.js +1310 -0
  11. package/lib/commonjs/network/components/NetworkHeaderLive.web.js +604 -0
  12. package/lib/commonjs/network/components/NetworkListScreen.js +2 -1
  13. package/lib/commonjs/network/components/NetworkListScreen.web.js +607 -0
  14. package/lib/commonjs/network/components/NetworkModal.js +12 -3
  15. package/lib/commonjs/network/components/NetworkNumberPrompt.web.js +232 -0
  16. package/lib/commonjs/network/components/NetworkOverrideEditor.web.js +942 -0
  17. package/lib/commonjs/network/components/NetworkOverrideHeaderButton.web.js +133 -0
  18. package/lib/commonjs/network/components/NetworkOverridesButton.web.js +143 -0
  19. package/lib/commonjs/network/components/NetworkOverridesScreen.js +160 -126
  20. package/lib/commonjs/network/components/NetworkOverridesScreen.web.js +830 -0
  21. package/lib/commonjs/network/components/NetworkSavedScreen.web.js +459 -0
  22. package/lib/commonjs/network/components/response-editor/Explorer.web.js +1136 -0
  23. package/lib/commonjs/network/hooks/useNetworkEventList.js +12 -2
  24. package/lib/commonjs/network/overrides/applyToXhr.js +276 -15
  25. package/lib/commonjs/network/utils/generateNetworkCopyText.js +32 -5
  26. package/lib/commonjs/network/utils/networkEventStore.js +12 -4
  27. package/lib/commonjs/network/utils/networkListener.js +1 -16
  28. package/lib/module/index.web.js +81 -0
  29. package/lib/module/network/components/DataViewer.web.js +875 -0
  30. package/lib/module/network/components/NetworkCopyScreen.web.js +44 -0
  31. package/lib/module/network/components/NetworkCopySettingsView.web.js +1237 -0
  32. package/lib/module/network/components/NetworkDetailScreen.web.js +97 -0
  33. package/lib/module/network/components/NetworkDetailStepper.web.js +294 -0
  34. package/lib/module/network/components/NetworkEventDetailView.web.js +1496 -0
  35. package/lib/module/network/components/NetworkEventItemCompact.web.js +800 -0
  36. package/lib/module/network/components/NetworkFilterScreen.web.js +51 -0
  37. package/lib/module/network/components/NetworkFilterViewV3.web.js +1306 -0
  38. package/lib/module/network/components/NetworkHeaderLive.web.js +598 -0
  39. package/lib/module/network/components/NetworkListScreen.js +2 -1
  40. package/lib/module/network/components/NetworkListScreen.web.js +605 -0
  41. package/lib/module/network/components/NetworkModal.js +12 -3
  42. package/lib/module/network/components/NetworkNumberPrompt.web.js +230 -0
  43. package/lib/module/network/components/NetworkOverrideEditor.web.js +939 -0
  44. package/lib/module/network/components/NetworkOverrideHeaderButton.web.js +131 -0
  45. package/lib/module/network/components/NetworkOverridesButton.web.js +141 -0
  46. package/lib/module/network/components/NetworkOverridesScreen.js +161 -127
  47. package/lib/module/network/components/NetworkOverridesScreen.web.js +826 -0
  48. package/lib/module/network/components/NetworkSavedScreen.web.js +457 -0
  49. package/lib/module/network/components/response-editor/Explorer.web.js +1133 -0
  50. package/lib/module/network/hooks/useNetworkEventList.js +12 -2
  51. package/lib/module/network/overrides/applyToXhr.js +275 -15
  52. package/lib/module/network/utils/generateNetworkCopyText.js +28 -1
  53. package/lib/module/network/utils/networkEventStore.js +12 -4
  54. package/lib/module/network/utils/networkListener.js +1 -16
  55. package/lib/typescript/index.web.d.ts +56 -0
  56. package/lib/typescript/index.web.d.ts.map +1 -0
  57. package/lib/typescript/network/components/DataViewer.web.d.ts +78 -0
  58. package/lib/typescript/network/components/DataViewer.web.d.ts.map +1 -0
  59. package/lib/typescript/network/components/NetworkCopyScreen.web.d.ts +27 -0
  60. package/lib/typescript/network/components/NetworkCopyScreen.web.d.ts.map +1 -0
  61. package/lib/typescript/network/components/NetworkCopySettingsView.web.d.ts +68 -0
  62. package/lib/typescript/network/components/NetworkCopySettingsView.web.d.ts.map +1 -0
  63. package/lib/typescript/network/components/NetworkDetailScreen.web.d.ts +45 -0
  64. package/lib/typescript/network/components/NetworkDetailScreen.web.d.ts.map +1 -0
  65. package/lib/typescript/network/components/NetworkDetailStepper.web.d.ts +40 -0
  66. package/lib/typescript/network/components/NetworkDetailStepper.web.d.ts.map +1 -0
  67. package/lib/typescript/network/components/NetworkEventDetailView.web.d.ts +67 -0
  68. package/lib/typescript/network/components/NetworkEventDetailView.web.d.ts.map +1 -0
  69. package/lib/typescript/network/components/NetworkEventItemCompact.web.d.ts +58 -0
  70. package/lib/typescript/network/components/NetworkEventItemCompact.web.d.ts.map +1 -0
  71. package/lib/typescript/network/components/NetworkFilterScreen.web.d.ts +28 -0
  72. package/lib/typescript/network/components/NetworkFilterScreen.web.d.ts.map +1 -0
  73. package/lib/typescript/network/components/NetworkFilterViewV3.web.d.ts +74 -0
  74. package/lib/typescript/network/components/NetworkFilterViewV3.web.d.ts.map +1 -0
  75. package/lib/typescript/network/components/NetworkHeaderLive.web.d.ts +112 -0
  76. package/lib/typescript/network/components/NetworkHeaderLive.web.d.ts.map +1 -0
  77. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  78. package/lib/typescript/network/components/NetworkListScreen.web.d.ts +63 -0
  79. package/lib/typescript/network/components/NetworkListScreen.web.d.ts.map +1 -0
  80. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  81. package/lib/typescript/network/components/NetworkNumberPrompt.web.d.ts +38 -0
  82. package/lib/typescript/network/components/NetworkNumberPrompt.web.d.ts.map +1 -0
  83. package/lib/typescript/network/components/NetworkOverrideEditor.web.d.ts +43 -0
  84. package/lib/typescript/network/components/NetworkOverrideEditor.web.d.ts.map +1 -0
  85. package/lib/typescript/network/components/NetworkOverrideHeaderButton.web.d.ts +32 -0
  86. package/lib/typescript/network/components/NetworkOverrideHeaderButton.web.d.ts.map +1 -0
  87. package/lib/typescript/network/components/NetworkOverridesButton.web.d.ts +26 -0
  88. package/lib/typescript/network/components/NetworkOverridesButton.web.d.ts.map +1 -0
  89. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts +9 -0
  90. package/lib/typescript/network/components/NetworkOverridesScreen.d.ts.map +1 -1
  91. package/lib/typescript/network/components/NetworkOverridesScreen.web.d.ts +69 -0
  92. package/lib/typescript/network/components/NetworkOverridesScreen.web.d.ts.map +1 -0
  93. package/lib/typescript/network/components/NetworkSavedScreen.web.d.ts +67 -0
  94. package/lib/typescript/network/components/NetworkSavedScreen.web.d.ts.map +1 -0
  95. package/lib/typescript/network/components/response-editor/Explorer.web.d.ts +56 -0
  96. package/lib/typescript/network/components/response-editor/Explorer.web.d.ts.map +1 -0
  97. package/lib/typescript/network/hooks/useNetworkEventList.d.ts +10 -0
  98. package/lib/typescript/network/hooks/useNetworkEventList.d.ts.map +1 -1
  99. package/lib/typescript/network/overrides/__tests__/applyToXhr.test.d.ts.map +1 -1
  100. package/lib/typescript/network/overrides/applyToXhr.d.ts +40 -14
  101. package/lib/typescript/network/overrides/applyToXhr.d.ts.map +1 -1
  102. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts +34 -1
  103. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -1
  104. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  105. package/lib/typescript/network/utils/networkListener.d.ts +0 -1
  106. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  107. package/package.json +2 -2
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * NetworkOverrideHeaderButton.web — DOM twin of
5
+ * NetworkOverrideHeaderButton.tsx.
6
+ *
7
+ * The Override action, in the request-detail header. A labelled word rather
8
+ * than an icon — it's the least familiar action in the tool, and "Override"
9
+ * spelled out is the entire explanation. A self-subscribing leaf: the
10
+ * selected id and the rule list are subscribed HERE so stepping between
11
+ * requests doesn't rebuild the header around a new prop.
12
+ *
13
+ * It lights up while a rule covers the request you're looking at — the same
14
+ * warning tint the OVERRIDDEN strip and the toolbar flask use. Tapping it
15
+ * goes straight to the Overrides screen with the rule open: a new one
16
+ * prefilled from this request (`draftFromEvent`), or the existing one if
17
+ * something already covers it (first enabled match wins, so stacking a
18
+ * second rule the first would shadow is never offered). (Native comments.)
19
+ *
20
+ * REUSED, not forked (all platform-free): `useOverrideRules`,
21
+ * `useSelectedNetworkEventId`, `ruleMatches`, `draftFromEvent`,
22
+ * `networkEventStore`, `networkSavedStore`.
23
+ *
24
+ * shared-ui RN chrome is replicated as DOM with provenance:
25
+ * - `macOSColors` — inlined verbatim subset from
26
+ * shared/src/ui/gameUI/constants/macOSDesignSystemColors.ts.
27
+ *
28
+ * @platform React DOM Web
29
+ */
30
+
31
+ import { useOverrideRules } from "../hooks/useOverrideRules";
32
+ import { useSelectedNetworkEventId } from "../hooks/useNetworkSelection";
33
+ import { ruleMatches } from "../overrides/matchRule";
34
+ import { draftFromEvent } from "../overrides/presets";
35
+ import { networkEventStore } from "../utils/networkEventStore";
36
+ import { networkSavedStore } from "../utils/networkSavedStore";
37
+
38
+ /** macOSColors subset — shared/src/ui/gameUI/constants/macOSDesignSystemColors.ts, verbatim. */
39
+ import { jsx as _jsx } from "react/jsx-runtime";
40
+ const macOSColors = {
41
+ background: {
42
+ input: "#26262A"
43
+ },
44
+ border: {
45
+ input: "#3D3D42"
46
+ },
47
+ text: {
48
+ primary: "#F5F5F7"
49
+ },
50
+ semantic: {
51
+ warning: "#FFEB3B",
52
+ warningBackground: "rgba(255, 235, 59, 0.15)"
53
+ }
54
+ };
55
+ export function NetworkOverrideHeaderButton({
56
+ onOpen
57
+ }) {
58
+ const eventId = useSelectedNetworkEventId();
59
+ const {
60
+ rules,
61
+ isRuleSpent
62
+ } = useOverrideRules();
63
+
64
+ // Resolved here rather than held as a prop: the live store has the fresh
65
+ // copy while a request is in flight, and the saved store is the only copy
66
+ // once it has been cleared or aged out. (Native comment.)
67
+ const event = eventId ? networkEventStore.getEvents().find(e => e.id === eventId) ?? networkSavedStore.getEventById(eventId) : null;
68
+ const existing = event ? rules.find(rule => rule.enabled && !isRuleSpent(rule) && ruleMatches(rule, event.url, String(event.method || "GET"))) ?? null : null;
69
+ const active = existing !== null;
70
+ const handlePress = () => {
71
+ if (!event) return;
72
+ if (existing) {
73
+ // Editing what's already there, rather than stacking a second rule the
74
+ // first would shadow — first enabled match wins.
75
+ const {
76
+ id,
77
+ hits: _h,
78
+ seen: _s,
79
+ createdAt: _c,
80
+ ...draft
81
+ } = existing;
82
+ onOpen(draft, id);
83
+ return;
84
+ }
85
+ onOpen(draftFromEvent(event));
86
+ };
87
+ return /*#__PURE__*/_jsx("button", {
88
+ type: "button",
89
+ onClick: handlePress,
90
+ "aria-label": active ? "Edit the override on this request" : "Override this request",
91
+ style: {
92
+ ...styles.button,
93
+ ...(active ? styles.buttonActive : null)
94
+ },
95
+ children: /*#__PURE__*/_jsx("span", {
96
+ style: {
97
+ ...styles.label,
98
+ ...(active ? styles.labelActive : null)
99
+ },
100
+ children: "Override"
101
+ })
102
+ });
103
+ }
104
+
105
+ // Values mirror the native StyleSheet block 1:1.
106
+ const styles = {
107
+ button: {
108
+ display: "inline-flex",
109
+ alignItems: "center",
110
+ padding: "6px 10px",
111
+ borderRadius: 6,
112
+ border: `1px solid ${macOSColors.border.input}`,
113
+ backgroundColor: macOSColors.background.input,
114
+ cursor: "pointer",
115
+ margin: 0,
116
+ boxSizing: "border-box"
117
+ },
118
+ buttonActive: {
119
+ border: `1px solid ${macOSColors.semantic.warning}77`,
120
+ backgroundColor: macOSColors.semantic.warningBackground
121
+ },
122
+ label: {
123
+ fontSize: 11,
124
+ fontWeight: 700,
125
+ color: macOSColors.text.primary,
126
+ fontFamily: "inherit"
127
+ },
128
+ labelActive: {
129
+ color: macOSColors.semantic.warning
130
+ }
131
+ };
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * NetworkOverridesButton.web — DOM twin of NetworkOverridesButton.tsx.
5
+ *
6
+ * Header button for the Overrides screen, and the always-visible "overrides
7
+ * are active" indicator. A self-subscribing LEAF, like the other live header
8
+ * pieces — the rule list changes on its own schedule (every hit bumps a
9
+ * counter) and the modal shell has no business re-rendering for it.
10
+ *
11
+ * The active tint is not decoration: a rule left enabled from yesterday makes
12
+ * the app behave wrongly in a way that looks exactly like a real bug, so the
13
+ * tool keeps saying so from the top level. (Native comment.)
14
+ *
15
+ * REUSED, not forked: `useOverrideRules` (platform-free).
16
+ *
17
+ * shared-ui RN chrome is replicated as DOM with provenance:
18
+ * - `macOSColors` — inlined verbatim subset from
19
+ * shared/src/ui/gameUI/constants/macOSDesignSystemColors.ts.
20
+ * - `FlaskConical` — inline lucide SVG (shared-ui icons are lucide
21
+ * re-exports).
22
+ *
23
+ * @platform React DOM Web
24
+ */
25
+
26
+ import { useOverrideRules } from "../hooks/useOverrideRules";
27
+
28
+ /** macOSColors subset — shared/src/ui/gameUI/constants/macOSDesignSystemColors.ts, verbatim. */
29
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
30
+ const macOSColors = {
31
+ background: {
32
+ base: "#0A0A0C",
33
+ hover: "#1D1D1F"
34
+ },
35
+ border: {
36
+ default: "#2D2D2F"
37
+ },
38
+ text: {
39
+ secondary: "#A1A1A6"
40
+ },
41
+ semantic: {
42
+ warning: "#FFEB3B",
43
+ warningBackground: "rgba(255, 235, 59, 0.15)"
44
+ }
45
+ };
46
+ const FlaskConical = ({
47
+ size = 14,
48
+ color
49
+ }) => /*#__PURE__*/_jsxs("svg", {
50
+ width: size,
51
+ height: size,
52
+ viewBox: "0 0 24 24",
53
+ fill: "none",
54
+ stroke: color,
55
+ strokeWidth: 2,
56
+ strokeLinecap: "round",
57
+ strokeLinejoin: "round",
58
+ style: {
59
+ flexShrink: 0,
60
+ display: "block"
61
+ },
62
+ children: [/*#__PURE__*/_jsx("path", {
63
+ d: "M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2"
64
+ }), /*#__PURE__*/_jsx("path", {
65
+ d: "M8.5 2h7"
66
+ }), /*#__PURE__*/_jsx("path", {
67
+ d: "M7 16h10"
68
+ })]
69
+ });
70
+ export function NetworkOverridesButton({
71
+ onPress
72
+ }) {
73
+ const {
74
+ activeCount
75
+ } = useOverrideRules();
76
+ const active = activeCount > 0;
77
+ return /*#__PURE__*/_jsxs("button", {
78
+ type: "button",
79
+ onClick: onPress,
80
+ style: {
81
+ ...styles.button,
82
+ ...(active ? styles.buttonActive : null)
83
+ },
84
+ "aria-label": active ? `Response overrides — ${activeCount} active` : "Response overrides",
85
+ children: [/*#__PURE__*/_jsx(FlaskConical, {
86
+ size: 14,
87
+ color: active ? macOSColors.semantic.warning : macOSColors.text.secondary
88
+ }), active ? /*#__PURE__*/_jsx("span", {
89
+ style: styles.badge,
90
+ children: /*#__PURE__*/_jsx("span", {
91
+ style: styles.badgeText,
92
+ children: activeCount
93
+ })
94
+ }) : null]
95
+ });
96
+ }
97
+
98
+ // Values mirror the native StyleSheet block 1:1: the header's standard 32×32
99
+ // action box; active fills the whole box the way the power toggle goes green,
100
+ // with the count badge INSIDE the box.
101
+ const styles = {
102
+ button: {
103
+ position: "relative",
104
+ display: "inline-flex",
105
+ width: 32,
106
+ height: 32,
107
+ boxSizing: "border-box",
108
+ borderRadius: 8,
109
+ alignItems: "center",
110
+ justifyContent: "center",
111
+ backgroundColor: macOSColors.background.hover,
112
+ border: `1px solid ${macOSColors.border.default}`,
113
+ cursor: "pointer",
114
+ padding: 0,
115
+ margin: 0
116
+ },
117
+ buttonActive: {
118
+ backgroundColor: macOSColors.semantic.warningBackground,
119
+ border: `1px solid ${macOSColors.semantic.warning}77`
120
+ },
121
+ badge: {
122
+ position: "absolute",
123
+ top: 1,
124
+ right: 1,
125
+ minWidth: 12,
126
+ height: 12,
127
+ borderRadius: 6,
128
+ padding: "0 3px",
129
+ boxSizing: "border-box",
130
+ display: "inline-flex",
131
+ alignItems: "center",
132
+ justifyContent: "center",
133
+ backgroundColor: macOSColors.semantic.warning
134
+ },
135
+ badgeText: {
136
+ fontSize: 8,
137
+ fontWeight: 800,
138
+ color: macOSColors.background.base,
139
+ lineHeight: 1
140
+ }
141
+ };
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from "react";
4
4
  import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native";
5
- import { AlertTriangle, EmptyState, FlaskConical, Plus, macOSColors } from "@buoy-gg/shared-ui";
5
+ import { AlertCircle, AlertTriangle, DevToolsCard, EmptyState, FlaskConical, MethodBadge, Plus, Trash2, macOSColors } from "@buoy-gg/shared-ui";
6
6
  import { useOverrideRules } from "../hooks/useOverrideRules";
7
7
  import { NetworkOverrideEditor } from "./NetworkOverrideEditor";
8
8
  import { overrideRulesStore } from "../overrides/overrideRulesStore";
@@ -37,101 +37,111 @@ function toDraft(rule) {
37
37
  return draft;
38
38
  }
39
39
 
40
- /** Plain-language names for statuses, so a row never reads as a bare number. */
41
- const STATUS_NAMES = {
42
- 200: "Success",
43
- 201: "Created",
44
- 400: "Bad request",
45
- 401: "Unauthorized",
46
- 403: "Forbidden",
47
- 404: "Not found",
48
- 429: "Rate limited",
49
- 500: "Server error",
50
- 503: "Unavailable"
51
- };
52
-
53
- /** What the rule DOES, in the same words the editor uses. */
54
- function outcomeLine(rule) {
55
- const parts = [];
56
- if (rule.kind === "respond") {
57
- const status = rule.status ?? 200;
58
- const name = STATUS_NAMES[status];
59
- parts.push(name ? `${name} ${status}` : String(status));
60
- } else if (rule.kind === "fail") {
61
- parts.push(rule.failKind === "timeout" ? "Timeout" : "Offline");
62
- } else {
63
- parts.push("Real response");
64
- }
65
- if (rule.delayMs) parts.push(`${rule.delayMs / 1000}s`);
66
- if (rule.alternate) parts.push("every other");else if (rule.times === 1) parts.push("once");else if (typeof rule.times === "number") parts.push(`${rule.times}×`);
67
- return parts.join(" · ");
68
- }
69
-
70
40
  /**
71
41
  * Where the rule applies — PATH only, not the whole URL.
72
42
  *
73
43
  * The host is the same for every rule in a typical app, so showing it pushes
74
- * the part that differs off the end of the line.
44
+ * the part that differs off the end of the line. Same reasoning as the request
45
+ * row's `displayUrl`.
75
46
  */
76
- function scopeLine(rule) {
77
- const method = rule.methods?.length ? rule.methods.join("/") : "Any";
78
- let path = rule.urlPattern;
47
+ function patternPath(rule) {
48
+ const path = rule.urlPattern;
79
49
  const schemeEnd = path.indexOf("://");
80
- if (schemeEnd !== -1) {
81
- const slash = path.indexOf("/", schemeEnd + 3);
82
- path = slash === -1 ? path.slice(schemeEnd + 3) : path.slice(slash);
83
- }
84
- return `${method} ${path}`;
50
+ if (schemeEnd === -1) return path;
51
+ const slash = path.indexOf("/", schemeEnd + 3);
52
+ return slash === -1 ? path.slice(schemeEnd + 3) : path.slice(slash);
53
+ }
54
+
55
+ /** Delay and lifetime, in the editor's own words. Empty for a plain rule. */
56
+ function extrasLine(rule) {
57
+ const parts = [];
58
+ if (rule.delayMs) parts.push(`${rule.delayMs / 1000}s delay`);
59
+ if (rule.alternate) parts.push("every other");else if (rule.times === 1) parts.push("once");else if (typeof rule.times === "number") parts.push(`${rule.times}×`);
60
+ return parts.join(" · ");
61
+ }
62
+
63
+ /** The outcome's color, on the request list's status scale — a forced 500
64
+ * should look exactly like a real one. */
65
+ function ruleAccent(rule) {
66
+ if (rule.kind === "fail") return macOSColors.semantic.error;
67
+ if (rule.kind === "delay") return macOSColors.semantic.info;
68
+ const status = rule.status ?? 200;
69
+ if (status >= 200 && status < 300) return macOSColors.semantic.success;
70
+ if (status >= 300 && status < 400) return macOSColors.semantic.info;
71
+ return macOSColors.semantic.error;
85
72
  }
86
73
 
87
74
  /**
88
- * One rule. Toggle · two lines · hit count, and nothing else.
89
- *
90
- * The pencil and trash icons are gone: the row itself opens the editor (where
91
- * Delete now lives), and three tap targets in a 44pt row is a config table, not
92
- * a list. `hits` stays as the only right-hand element because "my glob never
93
- * matched" is the most common rule bug and the count is how you find out.
75
+ * One rule, in the request row's visual language — the same card, method badge,
76
+ * monospace path and status colors as `NetworkEventItemCompact`, so a rule
77
+ * reads as "the request it will produce". What's different is what a rule
78
+ * needs: a trash instead of the type chip and chevron, and the outcome color
79
+ * only floods the left edge while the rule is live. There's no per-rule
80
+ * on/off — a rule you don't want anymore gets deleted, and pausing everything
81
+ * is the master bar's job.
94
82
  */
95
83
  function RuleRow({
96
84
  rule,
97
85
  spent,
98
- onToggle,
99
- onEdit
86
+ onEdit,
87
+ onDelete
100
88
  }) {
101
89
  const live = rule.enabled && !spent;
102
- const accent = live ? macOSColors.semantic.warning : macOSColors.text.muted;
103
- return /*#__PURE__*/_jsxs(View, {
104
- style: styles.row,
105
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
106
- onPress: onToggle,
107
- "sentry-label": "ignore toggle override rule",
108
- accessibilityLabel: live ? "Turn rule off" : "Turn rule on",
109
- style: [styles.toggle, live && {
110
- borderColor: accent,
111
- backgroundColor: `${accent}33`
112
- }],
113
- children: /*#__PURE__*/_jsx(View, {
114
- style: [styles.toggleDot, {
115
- backgroundColor: accent
116
- }]
117
- })
118
- }), /*#__PURE__*/_jsxs(TouchableOpacity, {
119
- onPress: onEdit,
120
- "sentry-label": "ignore edit override rule",
121
- style: styles.rowBody,
90
+ const accent = ruleAccent(rule);
91
+ const extras = extrasLine(rule);
92
+ const method = rule.methods?.length ? rule.methods.join("/") : "ANY";
93
+ return /*#__PURE__*/_jsxs(DevToolsCard, {
94
+ onPress: onEdit,
95
+ style: styles.cardWrapper,
96
+ contentStyle: [styles.card, {
97
+ borderLeftColor: live ? accent : "transparent"
98
+ }],
99
+ children: [/*#__PURE__*/_jsx(MethodBadge, {
100
+ method: method,
101
+ size: "small"
102
+ }), /*#__PURE__*/_jsxs(View, {
103
+ style: styles.cardMiddle,
122
104
  children: [/*#__PURE__*/_jsx(Text, {
123
- style: [styles.rowTitle, !live && styles.rowTitleOff],
124
- numberOfLines: 1,
125
- children: outcomeLine(rule)
126
- }), /*#__PURE__*/_jsx(Text, {
127
- style: styles.rowScope,
128
- numberOfLines: 1,
129
- children: scopeLine(rule)
130
- })]
105
+ style: [styles.urlText, !live && styles.urlTextOff],
106
+ numberOfLines: 2,
107
+ children: patternPath(rule)
108
+ }), extras ? /*#__PURE__*/_jsx(Text, {
109
+ style: styles.extrasText,
110
+ children: extras
111
+ }) : null]
131
112
  }), rule.hits > 0 ? /*#__PURE__*/_jsx(Text, {
132
113
  style: styles.rowHits,
133
114
  children: rule.hits
134
- }) : null]
115
+ }) : null, rule.kind === "fail" ? /*#__PURE__*/_jsxs(View, {
116
+ style: styles.failBadge,
117
+ children: [/*#__PURE__*/_jsx(AlertCircle, {
118
+ size: 10,
119
+ color: macOSColors.semantic.error
120
+ }), /*#__PURE__*/_jsx(Text, {
121
+ style: styles.failText,
122
+ children: rule.failKind === "timeout" ? "TIMEOUT" : "OFFLINE"
123
+ })]
124
+ }) : /*#__PURE__*/_jsx(Text, {
125
+ style: [styles.statusText, {
126
+ color: accent
127
+ }],
128
+ children: rule.kind === "delay" ? "PASS" : String(rule.status ?? 200)
129
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
130
+ onPress: onDelete,
131
+ "sentry-label": "ignore delete override rule",
132
+ accessibilityLabel: "Delete rule",
133
+ hitSlop: {
134
+ top: 8,
135
+ bottom: 8,
136
+ left: 4,
137
+ right: 4
138
+ },
139
+ style: styles.rowDelete,
140
+ children: /*#__PURE__*/_jsx(Trash2, {
141
+ size: 13,
142
+ color: macOSColors.text.muted
143
+ })
144
+ })]
135
145
  });
136
146
  }
137
147
  /**
@@ -155,6 +165,7 @@ function OverridesBody({
155
165
  onUpgradePress,
156
166
  pendingDraft,
157
167
  onPendingConsumed,
168
+ backHandlerRef,
158
169
  applySafeAreaInset
159
170
  }) {
160
171
  const {
@@ -176,6 +187,20 @@ function OverridesBody({
176
187
  overrideRulesStore.noteHealthyLaunch();
177
188
  }, []);
178
189
 
190
+ // Back while editing means "back to the rule list", not "leave overrides" —
191
+ // see the prop's comment. Cleared on unmount so a hidden screen can't
192
+ // swallow back presses.
193
+ useEffect(() => {
194
+ if (!backHandlerRef) return;
195
+ backHandlerRef.current = editing ? () => {
196
+ setEditing(null);
197
+ return true;
198
+ } : null;
199
+ return () => {
200
+ backHandlerRef.current = null;
201
+ };
202
+ }, [backHandlerRef, editing]);
203
+
179
204
  /**
180
205
  * Open a rule for editing, pulling its body over if this surface doesn't have
181
206
  * it.
@@ -251,8 +276,12 @@ function OverridesBody({
251
276
  };
252
277
  const handleSave = () => {
253
278
  if (!editing) return;
279
+ // Saving always arms the rule. With no per-rule toggle, this is the ONLY
280
+ // way back on for a rule the free tier disabled or that used itself up —
281
+ // and nobody edits a rule they want to stay inert.
254
282
  const draft = {
255
283
  ...editing.draft,
284
+ enabled: true,
256
285
  urlPattern: editing.draft.urlPattern.trim()
257
286
  };
258
287
  if (!draft.urlPattern) return;
@@ -260,21 +289,14 @@ function OverridesBody({
260
289
  ...draft,
261
290
  id: editing.id
262
291
  });
263
- if (draft.enabled) {
264
- enforceFreeTier(saved?.id ?? editing.id);
265
- // Arm the master switch. Deliberately creating a rule and having it sit
266
- // inert because overrides happen to be paused is the silent-failure this
267
- // feature can least afford — the preset path already does this, and the
268
- // manager disagreeing with it was just a gap.
269
- setEnabled(true);
270
- }
292
+ enforceFreeTier(saved?.id ?? editing.id);
293
+ // Arm the master switch. Deliberately creating a rule and having it sit
294
+ // inert because overrides happen to be paused is the silent-failure this
295
+ // feature can least afford — the preset path already does this, and the
296
+ // manager disagreeing with it was just a gap.
297
+ setEnabled(true);
271
298
  setEditing(null);
272
299
  };
273
- const handleToggle = rule => {
274
- const next = !rule.enabled;
275
- setRuleEnabled(rule.id, next);
276
- if (next) enforceFreeTier(rule.id);
277
- };
278
300
  if (editing) {
279
301
  return /*#__PURE__*/_jsx(NetworkOverrideEditor, {
280
302
  draft: editing.draft,
@@ -344,12 +366,12 @@ function OverridesBody({
344
366
  children: [rules.map(rule => /*#__PURE__*/_jsx(RuleRow, {
345
367
  rule: rule,
346
368
  spent: isRuleSpent(rule),
347
- onToggle: () => handleToggle(rule),
348
- onEdit: () => openRule(rule)
349
- }, rule.id)), /*#__PURE__*/_jsx(Text, {
369
+ onEdit: () => openRule(rule),
370
+ onDelete: () => removeRule(rule.id)
371
+ }, rule.id)), !isPro ? /*#__PURE__*/_jsx(Text, {
350
372
  style: styles.footNote,
351
- children: isPro ? "Dev builds only." : "Dev builds only. Free: one override at a time. Pro: up to 50."
352
- })]
373
+ children: "Free: one override at a time. Pro: up to 50."
374
+ }) : null]
353
375
  })]
354
376
  });
355
377
  }
@@ -426,54 +448,66 @@ const styles = StyleSheet.create({
426
448
  padding: 10,
427
449
  gap: 6
428
450
  },
429
- row: {
430
- flexDirection: "row",
431
- alignItems: "center",
432
- gap: 10,
433
- paddingHorizontal: 10,
434
- paddingVertical: 10,
435
- borderRadius: 6,
436
- borderWidth: 1,
437
- borderColor: macOSColors.border.default,
438
- backgroundColor: macOSColors.background.card
451
+ // The list already spaces rows via `listContent` gap — zero out the card's
452
+ // own margins so the two don't stack.
453
+ cardWrapper: {
454
+ marginHorizontal: 0,
455
+ marginVertical: 0
439
456
  },
440
- toggle: {
441
- width: 22,
442
- height: 22,
443
- borderRadius: 11,
444
- borderWidth: 1,
445
- borderColor: macOSColors.border.default,
457
+ card: {
458
+ flexDirection: "row",
446
459
  alignItems: "center",
447
- justifyContent: "center"
448
- },
449
- toggleDot: {
450
- width: 8,
451
- height: 8,
452
- borderRadius: 4
460
+ gap: 8,
461
+ borderLeftWidth: 3,
462
+ borderLeftColor: "transparent"
453
463
  },
454
- rowBody: {
464
+ cardMiddle: {
455
465
  flex: 1
456
466
  },
457
- rowTitle: {
467
+ // Same type as the request row's URL.
468
+ urlText: {
458
469
  fontSize: 12,
459
- fontWeight: "600",
460
- color: macOSColors.text.primary
470
+ color: macOSColors.text.primary,
471
+ lineHeight: 16,
472
+ fontFamily: "monospace"
461
473
  },
462
- rowTitleOff: {
474
+ urlTextOff: {
463
475
  color: macOSColors.text.muted
464
476
  },
465
- rowScope: {
477
+ extrasText: {
466
478
  fontSize: 10,
467
- fontFamily: "monospace",
468
479
  color: macOSColors.text.secondary,
469
480
  marginTop: 2
470
481
  },
482
+ statusText: {
483
+ fontSize: 11,
484
+ fontWeight: "600"
485
+ },
486
+ failBadge: {
487
+ flexDirection: "row",
488
+ alignItems: "center",
489
+ gap: 2,
490
+ paddingHorizontal: 4,
491
+ paddingVertical: 1,
492
+ backgroundColor: macOSColors.semantic.error + "26",
493
+ borderRadius: 3
494
+ },
495
+ failText: {
496
+ fontSize: 10,
497
+ color: macOSColors.semantic.error,
498
+ fontWeight: "600"
499
+ },
471
500
  rowHits: {
472
501
  fontSize: 11,
473
502
  fontWeight: "700",
474
- color: macOSColors.semantic.warning,
475
- minWidth: 16,
476
- textAlign: "right"
503
+ color: macOSColors.semantic.warning
504
+ },
505
+ rowDelete: {
506
+ padding: 5,
507
+ borderRadius: 5,
508
+ borderWidth: 1,
509
+ borderColor: macOSColors.border.default,
510
+ backgroundColor: macOSColors.background.base
477
511
  },
478
512
  footNote: {
479
513
  fontSize: 10,