@buoy-gg/network 5.0.0 → 6.0.0

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