@buoy-gg/network 5.0.0 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +29 -353
  2. package/lib/commonjs/index.js +14 -0
  3. package/lib/commonjs/network/components/NetworkEventItemCompact.js +32 -18
  4. package/lib/commonjs/network/components/NetworkHeaderLive.js +199 -0
  5. package/lib/commonjs/network/components/NetworkListScreen.js +203 -0
  6. package/lib/commonjs/network/components/NetworkModal.js +153 -630
  7. package/lib/commonjs/network/hooks/useNetworkBadgeCounts.js +83 -0
  8. package/lib/commonjs/network/hooks/useNetworkEventList.js +93 -0
  9. package/lib/commonjs/network/hooks/useNetworkHasMatches.js +56 -0
  10. package/lib/commonjs/network/utils/filterNetworkEvents.js +96 -0
  11. package/lib/commonjs/network/utils/generateNetworkCopyText.js +139 -0
  12. package/lib/module/index.js +5 -0
  13. package/lib/module/network/components/NetworkEventItemCompact.js +32 -18
  14. package/lib/module/network/components/NetworkHeaderLive.js +193 -0
  15. package/lib/module/network/components/NetworkListScreen.js +199 -0
  16. package/lib/module/network/components/NetworkModal.js +157 -634
  17. package/lib/module/network/hooks/useNetworkBadgeCounts.js +79 -0
  18. package/lib/module/network/hooks/useNetworkEventList.js +89 -0
  19. package/lib/module/network/hooks/useNetworkHasMatches.js +52 -0
  20. package/lib/module/network/utils/filterNetworkEvents.js +92 -0
  21. package/lib/module/network/utils/generateNetworkCopyText.js +135 -0
  22. package/lib/typescript/index.d.ts +2 -0
  23. package/lib/typescript/index.d.ts.map +1 -1
  24. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +0 -4
  25. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  26. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +52 -0
  27. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -0
  28. package/lib/typescript/network/components/NetworkListScreen.d.ts +27 -0
  29. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -0
  30. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  31. package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts +24 -0
  32. package/lib/typescript/network/hooks/useNetworkBadgeCounts.d.ts.map +1 -0
  33. package/lib/typescript/network/hooks/useNetworkEventList.d.ts +34 -0
  34. package/lib/typescript/network/hooks/useNetworkEventList.d.ts.map +1 -0
  35. package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts +23 -0
  36. package/lib/typescript/network/hooks/useNetworkHasMatches.d.ts.map +1 -0
  37. package/lib/typescript/network/utils/filterNetworkEvents.d.ts +13 -0
  38. package/lib/typescript/network/utils/filterNetworkEvents.d.ts.map +1 -0
  39. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts +11 -0
  40. package/lib/typescript/network/utils/generateNetworkCopyText.d.ts.map +1 -0
  41. package/package.json +3 -3
@@ -7,32 +7,99 @@ exports.NetworkModal = NetworkModal;
7
7
  var _react = require("react");
8
8
  var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
- var _NetworkEventItemCompact = require("./NetworkEventItemCompact");
11
10
  var _NetworkFilterViewV = require("./NetworkFilterViewV3");
12
11
  var _useTickEveryMinute = require("../hooks/useTickEveryMinute");
13
12
  var _NetworkEventDetailView = require("./NetworkEventDetailView");
14
13
  var _NetworkCopySettingsView = require("./NetworkCopySettingsView");
15
- var _useNetworkEvents = require("../hooks/useNetworkEvents");
16
- var _formatting = require("../utils/formatting");
14
+ var _NetworkHeaderLive = require("./NetworkHeaderLive");
15
+ var _NetworkListScreen = require("./NetworkListScreen");
16
+ var _useNetworkEventList = require("../hooks/useNetworkEventList");
17
17
  var _jsxRuntime = require("react/jsx-runtime");
18
- // Decompose by Responsibility: Extract empty state component
19
- function EmptyState({
20
- isEnabled
18
+ // ============================================================================
19
+ // Screens (rule4 stable router: each gates itself with `return null`; the
20
+ // root renders all four as stable siblings instead of a nested ternary).
21
+ // Subscribing screens run their hooks unconditionally and pass
22
+ // `paused || !visible` so hidden screens don't subscribe.
23
+ // ============================================================================
24
+
25
+ function NetworkDetailScreen({
26
+ visible,
27
+ event,
28
+ ignoredPatternValues,
29
+ onTogglePattern
21
30
  }) {
22
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
23
- style: styles.emptyState,
24
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Globe, {
25
- size: 32,
26
- color: _sharedUi.macOSColors.text.muted
27
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
28
- style: styles.emptyTitle,
29
- children: "No network events"
30
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
31
- style: styles.emptyText,
32
- children: isEnabled ? "Network requests will appear here" : "Enable interception to start capturing"
33
- })]
31
+ if (!visible || !event) return null;
32
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
33
+ event: event,
34
+ ignoredPatterns: ignoredPatternValues,
35
+ onTogglePattern: onTogglePattern
36
+ });
37
+ }
38
+ function NetworkFilterScreen({
39
+ visible,
40
+ paused,
41
+ isPro,
42
+ filter,
43
+ onFilterChange,
44
+ ignoredPatterns,
45
+ onRemovePattern,
46
+ onAddPattern,
47
+ onTogglePatternMode
48
+ }) {
49
+ // Facet counts read the same filtered list the user is looking at.
50
+ const {
51
+ events
52
+ } = (0, _useNetworkEventList.useNetworkEventList)({
53
+ filter,
54
+ ignoredPatterns,
55
+ paused: paused || !visible,
56
+ isPro
57
+ });
58
+ if (!visible) return null;
59
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkFilterViewV.NetworkFilterViewV3, {
60
+ events: events,
61
+ filter: filter,
62
+ onFilterChange: onFilterChange,
63
+ ignoredPatterns: ignoredPatterns,
64
+ onRemovePattern: onRemovePattern,
65
+ onAddPattern: onAddPattern,
66
+ onTogglePatternMode: onTogglePatternMode
34
67
  });
35
68
  }
69
+ function NetworkCopyScreen({
70
+ visible,
71
+ paused,
72
+ isPro,
73
+ filter,
74
+ ignoredPatterns,
75
+ settings,
76
+ onSettingsChange
77
+ }) {
78
+ const {
79
+ events
80
+ } = (0, _useNetworkEventList.useNetworkEventList)({
81
+ filter,
82
+ ignoredPatterns,
83
+ paused: paused || !visible,
84
+ isPro
85
+ });
86
+ if (!visible) return null;
87
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkCopySettingsView.NetworkCopySettingsView, {
88
+ settings: settings,
89
+ onSettingsChange: onSettingsChange,
90
+ events: events
91
+ });
92
+ }
93
+
94
+ // ============================================================================
95
+ // Root — UI-state orchestration ONLY (rule3). No event-store subscription
96
+ // lives here: live data is consumed by the header leaves
97
+ // (NetworkHeaderLive) and the screens, so network events never re-render
98
+ // the modal shell/header. Profiling evidence: tool-bench 2026-07-06 —
99
+ // with the subscription here, 15 events re-rendered JsModal + every
100
+ // header button 15× and all visible rows 105×.
101
+ // ============================================================================
102
+
36
103
  function NetworkModalInner({
37
104
  visible,
38
105
  onClose,
@@ -43,44 +110,19 @@ function NetworkModalInner({
43
110
  const {
44
111
  isPro
45
112
  } = (0, _sharedUi.useFeatureGate)();
46
- const {
47
- events,
48
- allEvents,
49
- stats,
50
- filter,
51
- setFilter,
52
- clearEvents,
53
- isCapturing,
54
- pauseCapturing,
55
- resumeCapturing,
56
- hasLockedEvents,
57
- lockedEventCount,
58
- isEventLocked,
59
- requestLimit
60
- } = (0, _useNetworkEvents.useNetworkEvents)({
61
- isPro
62
- });
63
- const isEnabled = isCapturing;
64
- const toggleInterception = (0, _react.useCallback)(() => {
65
- if (isCapturing) {
66
- pauseCapturing();
67
- } else {
68
- resumeCapturing();
69
- }
70
- }, [isCapturing, pauseCapturing, resumeCapturing]);
71
- const handleModeChange = (0, _react.useCallback)(_mode => {
72
- // Mode changes handled by JsModal
73
- }, []);
113
+
114
+ // Capture pause (the power toggle's freeze-stream semantic) — gates
115
+ // every subscriber in this modal's tree via props.
116
+ const [paused, setPaused] = (0, _react.useState)(false);
117
+ const [filter, setFilter] = (0, _react.useState)({});
74
118
  const [selectedEvent, setSelectedEvent] = (0, _react.useState)(null);
75
119
  const [showFilterView, setShowFilterView] = (0, _react.useState)(false);
76
120
  const [activeTab, setActiveTab] = (0, _react.useState)("filters");
77
121
  const [searchText, setSearchText] = (0, _react.useState)("");
78
122
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
79
123
  const searchInputRef = (0, _react.useRef)(null);
80
- // Ignored domains/URL patterns now live in a shared, persisted singleton store
81
- // (@buoy-gg/shared-ui) so the Network tool and the Events tool stay in sync and
82
- // share the exact same filter logic. The four handlers below are thin wrappers
83
- // exposed by the hook; `values` is the derived Set used for presence checks.
124
+ // Ignored domains/URL patterns live in a shared, persisted singleton store
125
+ // (@buoy-gg/shared-ui) so the Network tool and the Events tool stay in sync.
84
126
  const {
85
127
  patterns: ignoredPatterns,
86
128
  values: ignoredPatternValues,
@@ -90,7 +132,6 @@ function NetworkModalInner({
90
132
  toggleMode: toggleIgnoredPatternMode
91
133
  } = (0, _sharedUi.useIgnoredPatterns)();
92
134
  const [copySettings, setCopySettings] = (0, _react.useState)(_NetworkCopySettingsView.DEFAULT_COPY_SETTINGS);
93
- const flatListRef = (0, _react.useRef)(null);
94
135
  const hasLoadedCopySettings = (0, _react.useRef)(false);
95
136
  const [showUpgradeModal, setShowUpgradeModal] = (0, _react.useState)(false);
96
137
 
@@ -135,9 +176,6 @@ function NetworkModalInner({
135
176
  }, [copySettings]);
136
177
 
137
178
  // Simple handlers - no useCallback needed per rule2
138
- const handleEventPress = event => {
139
- setSelectedEvent(event);
140
- };
141
179
  const handleBack = () => {
142
180
  setSelectedEvent(null);
143
181
  };
@@ -148,6 +186,15 @@ function NetworkModalInner({
148
186
  searchText: text
149
187
  }));
150
188
  };
189
+ const toggleStatusFilter = status => {
190
+ setFilter(prev => ({
191
+ ...prev,
192
+ status: prev.status === status ? undefined : status
193
+ }));
194
+ };
195
+ const handleModeChange = _mode => {
196
+ // Mode changes handled by JsModal
197
+ };
151
198
  (0, _react.useEffect)(() => {
152
199
  if (isSearchActive) {
153
200
  requestAnimationFrame(() => {
@@ -156,226 +203,10 @@ function NetworkModalInner({
156
203
  }
157
204
  }, [isSearchActive]);
158
205
 
159
- // Filter events based on ignored patterns (mode-aware)
160
- const filteredEvents = (0, _react.useMemo)(() => {
161
- if (ignoredPatterns.length === 0) return events;
162
- return events.filter(event => {
163
- const isFiltered = ignoredPatterns.some(pattern => (0, _sharedUi.urlMatchesIgnoredPattern)(event.url, pattern));
164
- return !isFiltered;
165
- });
166
- }, [events, ignoredPatterns]);
167
-
168
- // Helper to check if payload should be included based on size
169
- const shouldIncludePayload = (0, _react.useCallback)(data => {
170
- if (copySettings.bodySizeThreshold === -1) return true;
171
- if (!data) return true;
172
- try {
173
- const size = JSON.stringify(data).length;
174
- return size < copySettings.bodySizeThreshold * 1024;
175
- } catch {
176
- return false;
177
- }
178
- }, [copySettings.bodySizeThreshold]);
179
-
180
- // Generate copy text based on settings
181
- const generateCopyText = (0, _react.useCallback)(() => {
182
- let eventsToUse = filteredEvents;
183
-
184
- // Apply filter mode
185
- if (copySettings.filterMode === "failed") {
186
- eventsToUse = eventsToUse.filter(e => e.error || e.status && e.status >= 400);
187
- } else if (copySettings.filterMode === "success") {
188
- eventsToUse = eventsToUse.filter(e => e.status && e.status >= 200 && e.status < 300);
189
- }
190
- if (eventsToUse.length === 0) {
191
- return "No requests to copy";
192
- }
193
-
194
- // Plain text format (URLs only)
195
- if (copySettings.format === "plaintext") {
196
- return eventsToUse.map(event => `${event.method} ${event.url}`).join("\n");
197
- }
198
-
199
- // JSON format
200
- if (copySettings.format === "json") {
201
- const requests = eventsToUse.map(event => {
202
- const obj = {};
203
- if (copySettings.includeMethod) {
204
- obj.method = event.method;
205
- obj.url = event.url;
206
- }
207
- if (copySettings.includeStatus) {
208
- obj.status = event.status;
209
- obj.statusText = event.statusText;
210
- }
211
- if (copySettings.includeDuration) obj.duration = event.duration;
212
- if (copySettings.includeTimestamp) obj.timestamp = event.timestamp;
213
- if (copySettings.includeClient) obj.requestClient = event.requestClient;
214
- if (copySettings.includeSizes) {
215
- obj.requestSize = event.requestSize;
216
- obj.responseSize = event.responseSize;
217
- }
218
- if (copySettings.includeErrors && event.error) obj.error = event.error;
219
- if (copySettings.includeRequestHeaders) {
220
- obj.requestHeaders = event.requestHeaders;
221
- }
222
- if (copySettings.includeResponseHeaders) {
223
- obj.responseHeaders = event.responseHeaders;
224
- }
225
- if (copySettings.includeRequestBody && shouldIncludePayload(event.requestData)) {
226
- obj.requestData = event.requestData;
227
- }
228
- if (copySettings.includeResponseBody && shouldIncludePayload(event.responseData)) {
229
- obj.responseData = event.responseData;
230
- }
231
- return obj;
232
- });
233
- return JSON.stringify(requests, null, 2);
234
- }
235
-
236
- // Markdown format
237
- const requests = eventsToUse.map((event, index) => {
238
- let md = `# Request ${index + 1}\n\n`;
239
- if (copySettings.includeMethod) {
240
- md += `**Method:** ${event.method}\n`;
241
- md += `**URL:** ${event.url}\n`;
242
- }
243
- if (copySettings.includeStatus) {
244
- const status = event.status || "Pending";
245
- md += `**Status:** ${status}${event.statusText ? ` (${event.statusText})` : ""}\n`;
246
- }
247
- if (copySettings.includeClient && event.requestClient) {
248
- md += `**Client:** ${event.requestClient}\n`;
249
- }
250
- if (copySettings.includeDuration && event.duration) {
251
- md += `**Duration:** ${event.duration}ms\n`;
252
- }
253
- if (copySettings.includeTimestamp) {
254
- md += `**Timestamp:** ${new Date(event.timestamp).toISOString()}\n`;
255
- }
256
- if (copySettings.includeSizes) {
257
- const reqSize = event.requestSize ? (0, _formatting.formatBytes)(event.requestSize) : "N/A";
258
- const resSize = event.responseSize ? (0, _formatting.formatBytes)(event.responseSize) : "N/A";
259
- md += `**Request Size:** ${reqSize}\n`;
260
- md += `**Response Size:** ${resSize}\n`;
261
- }
262
- if (copySettings.includeErrors && event.error) {
263
- md += `**Error:** ${event.error}\n`;
264
- }
265
- if (copySettings.includeRequestHeaders && Object.keys(event.requestHeaders).length > 0) {
266
- md += `\n## Request Headers\n\`\`\`json\n${JSON.stringify(event.requestHeaders, null, 2)}\n\`\`\`\n`;
267
- }
268
- if (copySettings.includeRequestBody) {
269
- md += `\n## Request Data\n`;
270
- if (shouldIncludePayload(event.requestData)) {
271
- md += `\`\`\`json\n${JSON.stringify(event.requestData, null, 2)}\n\`\`\`\n`;
272
- } else {
273
- const size = event.requestSize ? (0, _formatting.formatBytes)(event.requestSize) : "Unknown size";
274
- md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
275
- }
276
- }
277
- if (copySettings.includeResponseHeaders && Object.keys(event.responseHeaders).length > 0) {
278
- md += `\n## Response Headers\n\`\`\`json\n${JSON.stringify(event.responseHeaders, null, 2)}\n\`\`\`\n`;
279
- }
280
- if (copySettings.includeResponseBody) {
281
- md += `\n## Response Data\n`;
282
- if (shouldIncludePayload(event.responseData)) {
283
- md += `\`\`\`json\n${JSON.stringify(event.responseData, null, 2)}\n\`\`\`\n`;
284
- } else {
285
- const size = event.responseSize ? (0, _formatting.formatBytes)(event.responseSize) : "Unknown size";
286
- md += `_Payload omitted (${size}). Exceeds ${copySettings.bodySizeThreshold}KB threshold._\n`;
287
- }
288
- }
289
- md += `\n---\n`;
290
- return md;
291
- }).join("\n");
292
- const header = `# Network Requests (${eventsToUse.length} total)\n\n`;
293
- return header + requests;
294
- }, [filteredEvents, copySettings, shouldIncludePayload]);
295
-
296
- // Memoized copy text value for CopyButton
297
- const copyTextValue = (0, _react.useMemo)(() => {
298
- if (filteredEvents.length === 0) return "";
299
- return generateCopyText();
300
- }, [filteredEvents.length, generateCopyText]);
301
-
302
- // Compute badge stats from all events (before status filtering) with ignored patterns applied.
303
- // This ensures counts stay stable when toggling between status filters.
304
- const badgeStats = (0, _react.useMemo)(() => {
305
- let successful = 0;
306
- let failed = 0;
307
- let pending = 0;
308
- const eventsForStats = ignoredPatterns.length === 0 ? allEvents : allEvents.filter(event => !ignoredPatterns.some(pattern => (0, _sharedUi.urlMatchesIgnoredPattern)(event.url, pattern)));
309
- for (const event of eventsForStats) {
310
- if (event.status && event.status >= 200 && event.status < 300) {
311
- successful++;
312
- } else if (event.error || event.status && event.status >= 400) {
313
- failed++;
314
- } else if (!event.status && !event.error) {
315
- pending++;
316
- }
317
- }
318
- return {
319
- successful,
320
- failed,
321
- pending
322
- };
323
- }, [allEvents, ignoredPatterns]);
324
-
325
- // FlatList optimization - only keep what's needed for FlatList performance
326
- const keyExtractor = item => item.id;
327
-
328
- // Filter to only show unlocked events in the list
329
- const unlockedEvents = (0, _react.useMemo)(() => {
330
- return filteredEvents.filter(event => !isEventLocked(event.id));
331
- }, [filteredEvents, isEventLocked]);
332
-
333
- // Keep renderItem memoized for FlatList performance (justified by FlatList docs)
334
- const renderItem = (0, _react.useMemo)(() => {
335
- return ({
336
- item
337
- }) => {
338
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventItemCompact.NetworkEventItemCompact, {
339
- event: item,
340
- onPress: handleEventPress
341
- });
342
- };
343
- }, []); // handleEventPress defined inline
344
-
345
- // Footer component showing upgrade banner when events are locked
346
- const renderListFooter = (0, _react.useCallback)(() => {
347
- if (!hasLockedEvents) return null;
348
- const isSearching = searchText.length > 0;
349
- const bannerText = isSearching ? `${lockedEventCount} matching ${lockedEventCount === 1 ? 'request' : 'requests'} in locked history` : `${lockedEventCount} requests locked`;
350
- const subtextMessage = isSearching ? "Upgrade to Pro to search your full request history" : "Upgrade to Pro to unlock full request history";
351
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
352
- style: styles.limitBanner,
353
- onPress: () => setShowUpgradeModal(true),
354
- activeOpacity: 0.8,
355
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
356
- style: styles.limitBannerContent,
357
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
358
- style: styles.limitBannerLeft,
359
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
360
- size: 16,
361
- color: "#20C997"
362
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
363
- style: styles.limitBannerText,
364
- children: bannerText
365
- })]
366
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProBadge, {})]
367
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
368
- style: styles.limitBannerSubtext,
369
- children: subtextMessage
370
- })]
371
- });
372
- }, [hasLockedEvents, lockedEventCount, searchText]);
373
-
374
206
  // Compact header with actions (like Sentry/Storage modals)
375
207
  const renderHeaderContent = () => {
376
208
  // Filter view header with tabs
377
209
  if (showFilterView) {
378
- // Copy tab is always visible and clickable - gating handled inside the view
379
210
  const tabs = [{
380
211
  key: "filters",
381
212
  label: "Filters"
@@ -410,7 +241,9 @@ function NetworkModalInner({
410
241
  });
411
242
  }
412
243
 
413
- // Main list view header with search and filters
244
+ // Main list view header with search and filters. The live pieces
245
+ // (count chips, copy, trash) are self-subscribing leaves — this
246
+ // header function itself only re-runs on user interaction.
414
247
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
415
248
  children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
416
249
  onBack: onBack
@@ -447,54 +280,11 @@ function NetworkModalInner({
447
280
  color: _sharedUi.macOSColors.text.secondary
448
281
  })
449
282
  }) : null]
450
- }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
451
- style: styles.headerChipRow,
452
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
453
- style: [styles.headerChip, filter.status === "success" && styles.headerChipActive],
454
- onPress: () => setFilter({
455
- ...filter,
456
- status: filter.status === "success" ? undefined : "success"
457
- }),
458
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CheckCircle, {
459
- size: 12,
460
- color: _sharedUi.macOSColors.semantic.success
461
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
462
- style: [styles.headerChipValue, {
463
- color: _sharedUi.macOSColors.semantic.success
464
- }],
465
- children: badgeStats.successful
466
- })]
467
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
468
- style: [styles.headerChip, filter.status === "error" && styles.headerChipActive],
469
- onPress: () => setFilter({
470
- ...filter,
471
- status: filter.status === "error" ? undefined : "error"
472
- }),
473
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.XCircle, {
474
- size: 12,
475
- color: _sharedUi.macOSColors.semantic.error
476
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
477
- style: [styles.headerChipValue, {
478
- color: _sharedUi.macOSColors.semantic.error
479
- }],
480
- children: badgeStats.failed
481
- })]
482
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
483
- style: [styles.headerChip, filter.status === "pending" && styles.headerChipActive],
484
- onPress: () => setFilter({
485
- ...filter,
486
- status: filter.status === "pending" ? undefined : "pending"
487
- }),
488
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Clock, {
489
- size: 12,
490
- color: _sharedUi.macOSColors.semantic.warning
491
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
492
- style: [styles.headerChipValue, {
493
- color: _sharedUi.macOSColors.semantic.warning
494
- }],
495
- children: badgeStats.pending
496
- })]
497
- })]
283
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkHeaderBadges, {
284
+ activeStatus: filter.status,
285
+ onToggleStatus: toggleStatusFilter,
286
+ ignoredPatterns: ignoredPatterns,
287
+ paused: paused
498
288
  })
499
289
  }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
500
290
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
@@ -515,44 +305,31 @@ function NetworkModalInner({
515
305
  size: 14,
516
306
  color: filter.status || filter.method || filter.contentType ? _sharedUi.macOSColors.semantic.info : _sharedUi.macOSColors.text.muted
517
307
  })
518
- }), isPro ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton, {
519
- value: copyTextValue,
520
- size: 14,
521
- buttonStyle: styles.headerActionButton,
522
- colors: {
523
- idle: filteredEvents.length > 0 ? _sharedUi.macOSColors.text.secondary : _sharedUi.macOSColors.text.disabled,
524
- success: _sharedUi.macOSColors.semantic.success,
525
- error: _sharedUi.macOSColors.semantic.error
526
- },
527
- disabled: filteredEvents.length === 0
528
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
529
- "sentry-label": "ignore copy pro",
530
- onPress: () => setShowUpgradeModal(true),
531
- style: [styles.headerActionButton, filteredEvents.length === 0 && styles.headerActionButtonDisabled],
532
- disabled: filteredEvents.length === 0,
533
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Copy, {
534
- size: 14,
535
- color: filteredEvents.length > 0 ? _sharedUi.macOSColors.text.secondary : _sharedUi.macOSColors.text.disabled
536
- })
308
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkHeaderCopyButton, {
309
+ isPro: isPro,
310
+ filter: filter,
311
+ ignoredPatterns: ignoredPatterns,
312
+ copySettings: copySettings,
313
+ paused: paused,
314
+ onUpgradePress: () => setShowUpgradeModal(true)
537
315
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
538
- isEnabled: isEnabled,
539
- onToggle: toggleInterception,
316
+ isEnabled: !paused,
317
+ onToggle: () => setPaused(prev => !prev),
540
318
  accessibilityLabel: "Toggle network interception"
541
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
542
- "sentry-label": "ignore clear events",
543
- onPress: clearEvents,
544
- style: [styles.headerActionButton, events.length === 0 && styles.headerActionButtonDisabled],
545
- disabled: events.length === 0,
546
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Trash2, {
547
- size: 14,
548
- color: events.length > 0 ? _sharedUi.macOSColors.text.muted : _sharedUi.macOSColors.text.disabled
549
- })
319
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkClearButton, {
320
+ paused: paused
550
321
  })]
551
322
  })]
552
323
  });
553
324
  };
554
325
  const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : _sharedUi.devToolsStorageKeys.network.modal();
555
326
  if (!visible) return null;
327
+
328
+ // The LIST screen owns its scrolling (FlatList virtualization requires
329
+ // it — inside JsModal's ScrollView wrapper every row mounts, which at
330
+ // the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
331
+ // NetworkListScreen). Detail/filter/copy still use the wrapper.
332
+ const listOwnsScroll = selectedEvent == null && !showFilterView;
556
333
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.JsModal, {
557
334
  visible: visible,
558
335
  onClose: onClose,
@@ -566,58 +343,43 @@ function NetworkModalInner({
566
343
  enablePersistence: true,
567
344
  initialMode: "bottomSheet",
568
345
  enableGlitchEffects: true,
346
+ disableScrollWrapper: listOwnsScroll,
569
347
  styles: {},
570
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
348
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
571
349
  style: styles.container,
572
- children: selectedEvent ?
573
- /*#__PURE__*/
574
- /* Show detail view if event is selected */
575
- (0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
350
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkDetailScreen, {
351
+ visible: selectedEvent != null,
576
352
  event: selectedEvent,
577
- ignoredPatterns: ignoredPatternValues,
353
+ ignoredPatternValues: ignoredPatternValues,
578
354
  onTogglePattern: toggleIgnoredPattern
579
- }) : showFilterView ? (/* Show filter/copy view based on active tab */
580
- activeTab === "copy" ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkCopySettingsView.NetworkCopySettingsView, {
581
- settings: copySettings,
582
- onSettingsChange: setCopySettings,
583
- events: filteredEvents
584
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkFilterViewV.NetworkFilterViewV3, {
585
- events: filteredEvents,
355
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
356
+ visible: showFilterView && activeTab === "filters" && !selectedEvent,
357
+ paused: paused,
358
+ isPro: isPro,
586
359
  filter: filter,
587
360
  onFilterChange: setFilter,
588
361
  ignoredPatterns: ignoredPatterns,
589
362
  onRemovePattern: removeIgnoredPattern,
590
363
  onAddPattern: addIgnoredPattern,
591
364
  onTogglePatternMode: toggleIgnoredPatternMode
592
- })) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
593
- children: [!isEnabled ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
594
- style: styles.disabledBanner,
595
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
596
- size: 14,
597
- color: _sharedUi.macOSColors.semantic.warning
598
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
599
- style: styles.disabledText,
600
- children: "Network interception is disabled"
601
- })]
602
- }) : null, unlockedEvents.length > 0 || hasLockedEvents ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
603
- ref: flatListRef,
604
- data: unlockedEvents,
605
- renderItem: renderItem,
606
- keyExtractor: keyExtractor,
607
- contentContainerStyle: styles.listContent,
608
- showsVerticalScrollIndicator: true,
609
- removeClippedSubviews: true,
610
- onEndReachedThreshold: 0.8,
611
- initialNumToRender: 10,
612
- maxToRenderPerBatch: 10,
613
- windowSize: 10,
614
- scrollEnabled: false,
615
- "sentry-label": "ignore network events list",
616
- ListFooterComponent: renderListFooter
617
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
618
- isEnabled: isEnabled
619
- })]
620
- })
365
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkCopyScreen, {
366
+ visible: showFilterView && activeTab === "copy" && !selectedEvent,
367
+ paused: paused,
368
+ isPro: isPro,
369
+ filter: filter,
370
+ ignoredPatterns: ignoredPatterns,
371
+ settings: copySettings,
372
+ onSettingsChange: setCopySettings
373
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
374
+ visible: selectedEvent == null && !showFilterView,
375
+ paused: paused,
376
+ filter: filter,
377
+ ignoredPatterns: ignoredPatterns,
378
+ isPro: isPro,
379
+ searchText: searchText,
380
+ onEventPress: setSelectedEvent,
381
+ onUpgradePress: () => setShowUpgradeModal(true)
382
+ })]
621
383
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.UpgradeModal, {
622
384
  visible: showUpgradeModal,
623
385
  onClose: () => setShowUpgradeModal(false),
@@ -632,46 +394,6 @@ const styles = _reactNative.StyleSheet.create({
632
394
  flex: 1,
633
395
  backgroundColor: _sharedUi.macOSColors.background.base
634
396
  },
635
- // Compact header styles matching Sentry/Storage modals
636
- headerContainer: {
637
- flexDirection: "row",
638
- alignItems: "center",
639
- flex: 1,
640
- gap: 8,
641
- minHeight: 32,
642
- paddingLeft: 4
643
- },
644
- headerTitle: {
645
- color: _sharedUi.macOSColors.text.primary,
646
- fontSize: 14,
647
- fontWeight: "500",
648
- flex: 1,
649
- marginLeft: 8
650
- },
651
- headerStats: {
652
- display: "none"
653
- },
654
- headerStatsText: {
655
- fontSize: 12,
656
- color: _sharedUi.macOSColors.text.muted,
657
- fontWeight: "500"
658
- },
659
- headerFilteredText: {
660
- fontSize: 11,
661
- color: _sharedUi.macOSColors.semantic.warning,
662
- fontWeight: "500",
663
- marginLeft: 4
664
- },
665
- headerActions: {
666
- flexDirection: "row",
667
- gap: 6,
668
- marginLeft: "auto",
669
- marginRight: 4
670
- },
671
- headerCenterArea: {
672
- flex: 1,
673
- marginHorizontal: 8
674
- },
675
397
  headerSearchContainer: {
676
398
  flexDirection: "row",
677
399
  alignItems: "center",
@@ -693,39 +415,6 @@ const styles = _reactNative.StyleSheet.create({
693
415
  marginLeft: 6,
694
416
  padding: 4
695
417
  },
696
- headerChipRow: {
697
- flexDirection: "row",
698
- alignItems: "center",
699
- gap: 8
700
- },
701
- headerChip: {
702
- flexDirection: "row",
703
- alignItems: "center",
704
- gap: 4,
705
- backgroundColor: _sharedUi.macOSColors.background.hover,
706
- paddingHorizontal: 10,
707
- paddingVertical: 5,
708
- borderRadius: 12,
709
- borderWidth: 1,
710
- borderColor: _sharedUi.macOSColors.border.default
711
- },
712
- headerChipActive: {
713
- backgroundColor: _sharedUi.macOSColors.semantic.infoBackground,
714
- borderColor: _sharedUi.macOSColors.semantic.info + "50",
715
- shadowColor: _sharedUi.macOSColors.semantic.info,
716
- shadowOffset: {
717
- width: 0,
718
- height: 1
719
- },
720
- shadowOpacity: 0.08,
721
- shadowRadius: 2,
722
- elevation: 1
723
- },
724
- headerChipValue: {
725
- fontSize: 12,
726
- fontWeight: "600",
727
- fontFamily: "monospace"
728
- },
729
418
  headerActionButton: {
730
419
  width: 32,
731
420
  height: 32,
@@ -736,175 +425,9 @@ const styles = _reactNative.StyleSheet.create({
736
425
  alignItems: "center",
737
426
  justifyContent: "center"
738
427
  },
739
- headerActionButtonDisabled: {
740
- opacity: 0.55
741
- },
742
- // Shared navbar styles (matching React Query modal)
743
- tabNavigationContainer: {
744
- flexDirection: "row",
745
- backgroundColor: _sharedUi.macOSColors.background.card,
746
- borderRadius: 6,
747
- padding: 2,
748
- borderWidth: 1,
749
- borderColor: _sharedUi.macOSColors.border.default,
750
- justifyContent: "space-evenly",
751
- flex: 1,
752
- marginLeft: 8,
753
- marginRight: 8
754
- },
755
- tabButton: {
756
- paddingHorizontal: 8,
757
- paddingVertical: 5,
758
- borderRadius: 4,
759
- alignItems: "center",
760
- justifyContent: "center",
761
- flex: 1,
762
- marginHorizontal: 1
763
- },
764
- tabButtonActive: {
765
- backgroundColor: _sharedUi.macOSColors.semantic.infoBackground,
766
- borderWidth: 1,
767
- borderColor: _sharedUi.macOSColors.semantic.info + "40"
768
- },
769
- tabButtonInactive: {
770
- backgroundColor: "transparent"
771
- },
772
- tabButtonText: {
773
- fontSize: 12,
774
- fontWeight: "600",
775
- letterSpacing: 0.5,
776
- fontFamily: "monospace",
777
- textTransform: "uppercase"
778
- },
779
- tabButtonTextActive: {
780
- color: _sharedUi.macOSColors.semantic.info
781
- },
782
- tabButtonTextInactive: {
783
- color: _sharedUi.macOSColors.text.muted
784
- },
785
- startButton: {
786
- backgroundColor: _sharedUi.macOSColors.semantic.successBackground,
787
- borderColor: _sharedUi.macOSColors.semantic.success + "40"
788
- },
789
- stopButton: {
790
- backgroundColor: _sharedUi.macOSColors.semantic.errorBackground,
791
- borderColor: _sharedUi.macOSColors.semantic.error + "40"
792
- },
793
428
  activeFilterButton: {
794
429
  backgroundColor: _sharedUi.macOSColors.semantic.infoBackground,
795
430
  borderColor: _sharedUi.macOSColors.semantic.info + "40"
796
- },
797
- activeIgnoreButton: {
798
- backgroundColor: _sharedUi.macOSColors.semantic.warningBackground,
799
- borderColor: _sharedUi.macOSColors.semantic.warning + "33"
800
- },
801
- detailHeaderActions: {
802
- flexDirection: "row",
803
- gap: 6,
804
- marginLeft: "auto",
805
- marginRight: 4
806
- },
807
- // Search bar - minimal design with theme colors
808
- searchContainer: {
809
- display: "none"
810
- },
811
- searchInput: {},
812
- // Stats bar - minimal design
813
- statsBar: {
814
- display: "none"
815
- },
816
- statChip: {
817
- flexDirection: "row",
818
- alignItems: "center",
819
- gap: 4,
820
- backgroundColor: _sharedUi.macOSColors.background.hover,
821
- paddingHorizontal: 8,
822
- paddingVertical: 4,
823
- borderRadius: 12,
824
- borderWidth: 1,
825
- borderColor: "transparent"
826
- },
827
- statChipActive: {
828
- backgroundColor: _sharedUi.macOSColors.semantic.info + "26",
829
- borderColor: _sharedUi.macOSColors.semantic.info + "66"
830
- },
831
- statValue: {
832
- fontSize: 14,
833
- fontWeight: "600",
834
- fontFamily: "monospace"
835
- },
836
- statLabel: {
837
- fontSize: 10,
838
- color: _sharedUi.macOSColors.text.muted,
839
- fontWeight: "500",
840
- textTransform: "uppercase"
841
- },
842
- disabledBanner: {
843
- flexDirection: "row",
844
- alignItems: "center",
845
- gap: 8,
846
- padding: 10,
847
- marginHorizontal: 12,
848
- marginTop: 8,
849
- backgroundColor: _sharedUi.macOSColors.semantic.warningBackground,
850
- borderRadius: 8,
851
- borderWidth: 1,
852
- borderColor: _sharedUi.macOSColors.semantic.warning + "20"
853
- },
854
- disabledText: {
855
- color: _sharedUi.macOSColors.semantic.warning,
856
- fontSize: 11,
857
- flex: 1
858
- },
859
- listContent: {
860
- paddingTop: 8
861
- },
862
- emptyState: {
863
- alignItems: "center",
864
- paddingVertical: 40
865
- },
866
- emptyTitle: {
867
- color: _sharedUi.macOSColors.text.primary,
868
- fontSize: 14,
869
- fontWeight: "600",
870
- marginTop: 12,
871
- marginBottom: 6
872
- },
873
- emptyText: {
874
- color: _sharedUi.macOSColors.text.muted,
875
- fontSize: 12,
876
- textAlign: "center"
877
- },
878
- // Free tier limit banner
879
- limitBanner: {
880
- backgroundColor: "#1A1A1A",
881
- borderRadius: 10,
882
- borderWidth: 1,
883
- borderColor: "#20C997" + "40",
884
- padding: 12,
885
- marginHorizontal: 12,
886
- marginTop: 12,
887
- marginBottom: 34
888
- },
889
- limitBannerContent: {
890
- flexDirection: "row",
891
- alignItems: "center",
892
- justifyContent: "space-between",
893
- marginBottom: 4
894
- },
895
- limitBannerLeft: {
896
- flexDirection: "row",
897
- alignItems: "center",
898
- gap: 8
899
- },
900
- limitBannerText: {
901
- color: "#E0E0E0",
902
- fontSize: 13,
903
- fontWeight: "600"
904
- },
905
- limitBannerSubtext: {
906
- color: "#888888",
907
- fontSize: 11
908
431
  }
909
432
  });
910
433