@buoy-gg/network 1.7.2

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 (67) hide show
  1. package/README.md +381 -0
  2. package/lib/commonjs/index.js +34 -0
  3. package/lib/commonjs/network/components/NetworkCopySettingsView.js +867 -0
  4. package/lib/commonjs/network/components/NetworkEventDetailView.js +837 -0
  5. package/lib/commonjs/network/components/NetworkEventItemCompact.js +323 -0
  6. package/lib/commonjs/network/components/NetworkFilterViewV3.js +297 -0
  7. package/lib/commonjs/network/components/NetworkModal.js +937 -0
  8. package/lib/commonjs/network/hooks/useNetworkEvents.js +320 -0
  9. package/lib/commonjs/network/hooks/useTickEveryMinute.js +34 -0
  10. package/lib/commonjs/network/index.js +102 -0
  11. package/lib/commonjs/network/types/index.js +1 -0
  12. package/lib/commonjs/network/utils/extractOperationName.js +80 -0
  13. package/lib/commonjs/network/utils/formatGraphQLVariables.js +219 -0
  14. package/lib/commonjs/network/utils/formatting.js +30 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +269 -0
  16. package/lib/commonjs/network/utils/networkListener.js +801 -0
  17. package/lib/commonjs/package.json +1 -0
  18. package/lib/commonjs/preset.js +83 -0
  19. package/lib/module/index.js +7 -0
  20. package/lib/module/network/components/NetworkCopySettingsView.js +862 -0
  21. package/lib/module/network/components/NetworkEventDetailView.js +834 -0
  22. package/lib/module/network/components/NetworkEventItemCompact.js +320 -0
  23. package/lib/module/network/components/NetworkFilterViewV3.js +293 -0
  24. package/lib/module/network/components/NetworkModal.js +933 -0
  25. package/lib/module/network/hooks/useNetworkEvents.js +316 -0
  26. package/lib/module/network/hooks/useTickEveryMinute.js +29 -0
  27. package/lib/module/network/index.js +20 -0
  28. package/lib/module/network/types/index.js +1 -0
  29. package/lib/module/network/utils/extractOperationName.js +76 -0
  30. package/lib/module/network/utils/formatGraphQLVariables.js +213 -0
  31. package/lib/module/network/utils/formatting.js +9 -0
  32. package/lib/module/network/utils/networkEventStore.js +265 -0
  33. package/lib/module/network/utils/networkListener.js +791 -0
  34. package/lib/module/preset.js +79 -0
  35. package/lib/typescript/index.d.ts +3 -0
  36. package/lib/typescript/index.d.ts.map +1 -0
  37. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts +26 -0
  38. package/lib/typescript/network/components/NetworkCopySettingsView.d.ts.map +1 -0
  39. package/lib/typescript/network/components/NetworkEventDetailView.d.ts +13 -0
  40. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -0
  41. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  42. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -0
  43. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts +22 -0
  44. package/lib/typescript/network/components/NetworkFilterViewV3.d.ts.map +1 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts +14 -0
  46. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -0
  47. package/lib/typescript/network/hooks/useNetworkEvents.d.ts +72 -0
  48. package/lib/typescript/network/hooks/useNetworkEvents.d.ts.map +1 -0
  49. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts +9 -0
  50. package/lib/typescript/network/hooks/useTickEveryMinute.d.ts.map +1 -0
  51. package/lib/typescript/network/index.d.ts +12 -0
  52. package/lib/typescript/network/index.d.ts.map +1 -0
  53. package/lib/typescript/network/types/index.d.ts +88 -0
  54. package/lib/typescript/network/types/index.d.ts.map +1 -0
  55. package/lib/typescript/network/utils/extractOperationName.d.ts +41 -0
  56. package/lib/typescript/network/utils/extractOperationName.d.ts.map +1 -0
  57. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts +79 -0
  58. package/lib/typescript/network/utils/formatGraphQLVariables.d.ts.map +1 -0
  59. package/lib/typescript/network/utils/formatting.d.ts +6 -0
  60. package/lib/typescript/network/utils/formatting.d.ts.map +1 -0
  61. package/lib/typescript/network/utils/networkEventStore.d.ts +81 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -0
  63. package/lib/typescript/network/utils/networkListener.d.ts +191 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -0
  65. package/lib/typescript/preset.d.ts +76 -0
  66. package/lib/typescript/preset.d.ts.map +1 -0
  67. package/package.json +69 -0
@@ -0,0 +1,320 @@
1
+ "use strict";
2
+
3
+ import { memo } from "react";
4
+ import { StyleSheet, View, Text } from "react-native";
5
+ import { ChevronRight, Upload, Download, Clock, AlertCircle, ListItem, MethodBadge, TypeBadge, macOSColors } from "@buoy-gg/shared-ui";
6
+ import { formatBytes, formatDuration } from "../utils/formatting";
7
+ import { formatRelativeTime } from "@buoy-gg/shared-ui";
8
+ import { useTickEveryMinute } from "../hooks/useTickEveryMinute";
9
+ import { formatGraphQLDisplay } from "../utils/formatGraphQLVariables";
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ // Get color based on status
12
+ function getStatusColor(status, error) {
13
+ if (error) return macOSColors.semantic.error;
14
+ if (!status) return macOSColors.semantic.warning;
15
+ if (status >= 200 && status < 300) return macOSColors.semantic.success;
16
+ if (status >= 300 && status < 400) return macOSColors.semantic.info;
17
+ if (status >= 400) return macOSColors.semantic.error;
18
+ return macOSColors.text.muted;
19
+ }
20
+
21
+ // Get content type badge with color
22
+ function getContentTypeBadge(headers) {
23
+ const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
24
+ if (contentType.includes("json")) return "JSON";
25
+ if (contentType.includes("xml")) return "XML";
26
+ if (contentType.includes("html")) return "HTML";
27
+ if (contentType.includes("text")) return "TEXT";
28
+ if (contentType.includes("image")) return "IMG";
29
+ if (contentType.includes("video")) return "VIDEO";
30
+ if (contentType.includes("audio")) return "AUDIO";
31
+ if (contentType.includes("form")) return "FORM";
32
+ return null;
33
+ }
34
+
35
+ // Decomposed components following rule3 - Component Composition
36
+
37
+ // Status indicator component - single responsibility
38
+ function StatusIndicator({
39
+ event,
40
+ isPending,
41
+ statusColor
42
+ }) {
43
+ if (isPending) {
44
+ return /*#__PURE__*/_jsxs(View, {
45
+ style: styles.pendingBadge,
46
+ children: [/*#__PURE__*/_jsx(Clock, {
47
+ size: 10,
48
+ color: macOSColors.semantic.warning
49
+ }), /*#__PURE__*/_jsx(Text, {
50
+ style: styles.pendingText,
51
+ children: "..."
52
+ })]
53
+ });
54
+ }
55
+ if (event.error) {
56
+ return /*#__PURE__*/_jsxs(View, {
57
+ style: styles.errorBadge,
58
+ children: [/*#__PURE__*/_jsx(AlertCircle, {
59
+ size: 10,
60
+ color: macOSColors.semantic.error
61
+ }), /*#__PURE__*/_jsx(Text, {
62
+ style: styles.errorText,
63
+ children: "ERR"
64
+ })]
65
+ });
66
+ }
67
+ return /*#__PURE__*/_jsx(View, {
68
+ style: styles.statusBadge,
69
+ children: /*#__PURE__*/_jsx(Text, {
70
+ style: [styles.statusText, {
71
+ color: statusColor
72
+ }],
73
+ children: String(event.status)
74
+ })
75
+ });
76
+ }
77
+
78
+ // Size indicators component - single responsibility
79
+ function SizeIndicators({
80
+ requestSize,
81
+ responseSize
82
+ }) {
83
+ if (!requestSize && !responseSize) return null;
84
+ return /*#__PURE__*/_jsxs(View, {
85
+ style: styles.sizeRow,
86
+ children: [requestSize ? /*#__PURE__*/_jsxs(View, {
87
+ style: styles.sizeItem,
88
+ children: [/*#__PURE__*/_jsx(Upload, {
89
+ size: 8,
90
+ color: macOSColors.semantic.info
91
+ }), /*#__PURE__*/_jsx(Text, {
92
+ style: styles.sizeText,
93
+ children: formatBytes(requestSize)
94
+ })]
95
+ }) : null, responseSize ? /*#__PURE__*/_jsxs(View, {
96
+ style: styles.sizeItem,
97
+ children: [/*#__PURE__*/_jsx(Download, {
98
+ size: 8,
99
+ color: macOSColors.semantic.success
100
+ }), /*#__PURE__*/_jsx(Text, {
101
+ style: styles.sizeText,
102
+ children: formatBytes(responseSize)
103
+ })]
104
+ }) : null]
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Compact list-row representation of a network event. Optimized for large lists with memoization
110
+ * and periodic refresh via `useTickEveryMinute` to keep relative timestamps accurate.
111
+ */
112
+ export const NetworkEventItemCompact = /*#__PURE__*/memo(({
113
+ event,
114
+ onPress
115
+ }) => {
116
+ const tick = useTickEveryMinute();
117
+ const statusColor = getStatusColor(event.status, event.error);
118
+ const isPending = !event.status && !event.error;
119
+ const contentType = getContentTypeBadge(event.responseHeaders);
120
+
121
+ // Format URL for display (max 2 lines)
122
+ let displayUrl = event.path || event.url.replace(/^https?:\/\/[^/]+/, "");
123
+
124
+ // If this is a GraphQL request, show operation name with variables using arrow notation
125
+ // Matches React Query pattern: ["pokemon", "Sandshrew"] → "pokemon › Sandshrew"
126
+ if (event.requestClient === "graphql") {
127
+ if (event.operationName) {
128
+ // Format: GetPokemon › Sandshrew (matches React Query pattern)
129
+ displayUrl = formatGraphQLDisplay(event.operationName, event.graphqlVariables);
130
+ } else {
131
+ // If no operation name found, just remove the redundant /graphql path
132
+ displayUrl = displayUrl.replace(/\/graphql[^?]*/, "/graphql");
133
+ }
134
+ } else if (event.operationName) {
135
+ // For non-GraphQL requests with operation names (e.g., gRPC)
136
+ displayUrl = `${displayUrl}\n(${event.operationName})`;
137
+ }
138
+
139
+ // Format time with both absolute and relative
140
+ const timeString = new Date(event.timestamp).toLocaleTimeString("en-US", {
141
+ hour: "numeric",
142
+ minute: "2-digit",
143
+ second: "2-digit",
144
+ hour12: true
145
+ });
146
+ const relativeTime = formatRelativeTime(event.timestamp, tick);
147
+ return /*#__PURE__*/_jsxs(ListItem, {
148
+ onPress: () => onPress(event),
149
+ style: [styles.container, {
150
+ borderLeftColor: statusColor
151
+ }],
152
+ children: [/*#__PURE__*/_jsxs(View, {
153
+ style: styles.leftSection,
154
+ children: [/*#__PURE__*/_jsx(MethodBadge, {
155
+ method: event.method,
156
+ size: "small"
157
+ }), event.requestClient && /*#__PURE__*/_jsx(View, {
158
+ style: [styles.clientBadge, {
159
+ backgroundColor: event.requestClient === "fetch" ? "rgba(74, 144, 226, 0.15)" : event.requestClient === "graphql" ? "rgba(229, 53, 171, 0.15)" : event.requestClient === "grpc-web" ? "rgba(16, 185, 129, 0.15)" : "rgba(147, 51, 234, 0.15)"
160
+ }],
161
+ children: /*#__PURE__*/_jsx(Text, {
162
+ style: [styles.clientText, {
163
+ color: event.requestClient === "fetch" ? "#4A90E2" : event.requestClient === "graphql" ? "#E535AB" : event.requestClient === "grpc-web" ? "#10B981" : "#9333EA"
164
+ }],
165
+ children: event.requestClient === "graphql" ? "GQL" : event.requestClient === "grpc-web" ? "gRPC" : event.requestClient
166
+ })
167
+ }), /*#__PURE__*/_jsx(SizeIndicators, {
168
+ requestSize: event.requestSize,
169
+ responseSize: event.responseSize
170
+ })]
171
+ }), /*#__PURE__*/_jsx(View, {
172
+ style: styles.middleSection,
173
+ children: /*#__PURE__*/_jsx(Text, {
174
+ style: styles.urlText,
175
+ numberOfLines: 2,
176
+ children: displayUrl
177
+ })
178
+ }), /*#__PURE__*/_jsxs(View, {
179
+ style: styles.rightSection,
180
+ children: [/*#__PURE__*/_jsxs(View, {
181
+ style: styles.rightTopRow,
182
+ children: [/*#__PURE__*/_jsx(StatusIndicator, {
183
+ event: event,
184
+ isPending: isPending,
185
+ statusColor: statusColor
186
+ }), event.duration ? /*#__PURE__*/_jsx(Text, {
187
+ style: styles.durationText,
188
+ children: formatDuration(event.duration)
189
+ }) : null, contentType ? /*#__PURE__*/_jsx(TypeBadge, {
190
+ type: contentType,
191
+ size: "small"
192
+ }) : null]
193
+ }), /*#__PURE__*/_jsx(View, {
194
+ style: styles.rightBottomRow,
195
+ children: /*#__PURE__*/_jsxs(ListItem.Metadata, {
196
+ children: [timeString, " (", relativeTime, ")"]
197
+ })
198
+ })]
199
+ }), /*#__PURE__*/_jsx(ChevronRight, {
200
+ size: 14,
201
+ color: macOSColors.text.muted
202
+ })]
203
+ });
204
+ });
205
+ const styles = StyleSheet.create({
206
+ container: {
207
+ flexDirection: "row",
208
+ alignItems: "center",
209
+ backgroundColor: macOSColors.background.card,
210
+ borderRadius: 6,
211
+ paddingVertical: 8,
212
+ paddingHorizontal: 10,
213
+ paddingLeft: 8,
214
+ marginBottom: 4,
215
+ marginHorizontal: 12,
216
+ minHeight: 44,
217
+ borderLeftWidth: 3,
218
+ borderLeftColor: "transparent"
219
+ },
220
+ leftSection: {
221
+ marginRight: 8,
222
+ alignItems: "flex-start",
223
+ paddingTop: 2
224
+ },
225
+ middleSection: {
226
+ flex: 1,
227
+ justifyContent: "center",
228
+ paddingRight: 8
229
+ },
230
+ urlText: {
231
+ fontSize: 12,
232
+ color: macOSColors.text.primary,
233
+ lineHeight: 16,
234
+ fontFamily: "monospace"
235
+ },
236
+ rightSection: {
237
+ alignItems: "flex-end",
238
+ justifyContent: "center",
239
+ marginRight: 4
240
+ },
241
+ rightTopRow: {
242
+ flexDirection: "row",
243
+ alignItems: "center",
244
+ gap: 6,
245
+ marginBottom: 2
246
+ },
247
+ rightBottomRow: {
248
+ flexDirection: "row",
249
+ alignItems: "center",
250
+ gap: 4
251
+ },
252
+ statusBadge: {
253
+ paddingHorizontal: 4,
254
+ paddingVertical: 1,
255
+ borderRadius: 3
256
+ },
257
+ statusText: {
258
+ fontSize: 10,
259
+ fontWeight: "600"
260
+ },
261
+ pendingBadge: {
262
+ flexDirection: "row",
263
+ alignItems: "center",
264
+ gap: 2,
265
+ paddingHorizontal: 4,
266
+ paddingVertical: 1,
267
+ backgroundColor: macOSColors.semantic.warning + "26",
268
+ borderRadius: 3
269
+ },
270
+ pendingText: {
271
+ fontSize: 10,
272
+ color: macOSColors.semantic.warning,
273
+ fontWeight: "600"
274
+ },
275
+ errorBadge: {
276
+ flexDirection: "row",
277
+ alignItems: "center",
278
+ gap: 2,
279
+ paddingHorizontal: 4,
280
+ paddingVertical: 1,
281
+ backgroundColor: macOSColors.semantic.error + "26",
282
+ borderRadius: 3
283
+ },
284
+ errorText: {
285
+ fontSize: 10,
286
+ color: macOSColors.semantic.error,
287
+ fontWeight: "600"
288
+ },
289
+ durationText: {
290
+ fontSize: 9,
291
+ color: macOSColors.text.secondary
292
+ },
293
+ sizeRow: {
294
+ flexDirection: "row",
295
+ gap: 4,
296
+ marginTop: 4
297
+ },
298
+ sizeItem: {
299
+ flexDirection: "row",
300
+ alignItems: "center",
301
+ gap: 2
302
+ },
303
+ sizeText: {
304
+ fontSize: 8,
305
+ color: macOSColors.text.secondary,
306
+ fontFamily: "monospace"
307
+ },
308
+ clientBadge: {
309
+ paddingHorizontal: 4,
310
+ paddingVertical: 2,
311
+ borderRadius: 3,
312
+ marginTop: 4
313
+ },
314
+ clientText: {
315
+ fontSize: 8,
316
+ fontWeight: "700",
317
+ letterSpacing: 0.3,
318
+ textTransform: "uppercase"
319
+ }
320
+ });
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+
3
+ import { CheckCircle, Clock, DynamicFilterView, Film, FileJson, FileText, Filter, Globe, Image, macOSColors, Music, XCircle } from "@buoy-gg/shared-ui";
4
+ import { useCallback, useMemo } from "react";
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ function getContentType(event) {
7
+ const headers = event.responseHeaders || event.requestHeaders;
8
+ const contentType = headers?.["content-type"] || headers?.["Content-Type"] || "";
9
+ if (contentType.includes("json")) return {
10
+ type: "JSON",
11
+ color: macOSColors.semantic.info
12
+ };
13
+ if (contentType.includes("xml")) return {
14
+ type: "XML",
15
+ color: macOSColors.semantic.success
16
+ };
17
+ if (contentType.includes("html")) return {
18
+ type: "HTML",
19
+ color: macOSColors.semantic.warning
20
+ };
21
+ if (contentType.includes("text")) return {
22
+ type: "TEXT",
23
+ color: macOSColors.semantic.success
24
+ };
25
+ if (contentType.includes("image")) return {
26
+ type: "IMAGE",
27
+ color: macOSColors.semantic.error
28
+ };
29
+ if (contentType.includes("video")) return {
30
+ type: "VIDEO",
31
+ color: macOSColors.semantic.error
32
+ };
33
+ if (contentType.includes("audio")) return {
34
+ type: "AUDIO",
35
+ color: macOSColors.semantic.debug
36
+ };
37
+ if (contentType.includes("form")) return {
38
+ type: "FORM",
39
+ color: macOSColors.semantic.info
40
+ };
41
+ return {
42
+ type: "OTHER",
43
+ color: macOSColors.text.muted
44
+ };
45
+ }
46
+ function getContentTypeIcon(type) {
47
+ switch (type) {
48
+ case "JSON":
49
+ return FileJson;
50
+ case "HTML":
51
+ case "XML":
52
+ case "TEXT":
53
+ return FileText;
54
+ case "IMAGE":
55
+ return Image;
56
+ case "VIDEO":
57
+ return Film;
58
+ case "AUDIO":
59
+ return Music;
60
+ default:
61
+ return Globe;
62
+ }
63
+ }
64
+ function getMethodColor(method) {
65
+ switch (method) {
66
+ case "GET":
67
+ return macOSColors.semantic.success;
68
+ case "POST":
69
+ return macOSColors.semantic.info;
70
+ case "PUT":
71
+ return macOSColors.semantic.warning;
72
+ case "DELETE":
73
+ return macOSColors.semantic.error;
74
+ case "PATCH":
75
+ return macOSColors.semantic.success;
76
+ default:
77
+ return macOSColors.text.muted;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Control panel for filtering captured network events by status, method, host, and content type.
83
+ * Provides quick toggles, stats, and ignored-pattern management reminiscent of desktop tooling.
84
+ */
85
+ export function NetworkFilterViewV3({
86
+ events,
87
+ filter,
88
+ onFilterChange,
89
+ ignoredPatterns = new Set(),
90
+ onTogglePattern = () => {},
91
+ onAddPattern = () => {}
92
+ }) {
93
+ const statusCounts = useMemo(() => {
94
+ const counts = {
95
+ all: events.length,
96
+ success: 0,
97
+ error: 0,
98
+ pending: 0
99
+ };
100
+ events.forEach(event => {
101
+ if (event.error || event.status && event.status >= 400) {
102
+ counts.error += 1;
103
+ return;
104
+ }
105
+ if (event.status && event.status >= 200 && event.status < 300) {
106
+ counts.success += 1;
107
+ return;
108
+ }
109
+ if (!event.status && !event.error) {
110
+ counts.pending += 1;
111
+ }
112
+ });
113
+ return counts;
114
+ }, [events]);
115
+ const methodCounts = useMemo(() => {
116
+ return events.reduce((acc, event) => {
117
+ if (!event.method) return acc;
118
+ acc[event.method] = (acc[event.method] || 0) + 1;
119
+ return acc;
120
+ }, {});
121
+ }, [events]);
122
+ const contentTypeCounts = useMemo(() => {
123
+ return events.reduce((acc, event) => {
124
+ const {
125
+ type
126
+ } = getContentType(event);
127
+ acc[type] = (acc[type] || 0) + 1;
128
+ return acc;
129
+ }, {});
130
+ }, [events]);
131
+ const availableDomains = useMemo(() => {
132
+ const domains = new Set();
133
+ events.forEach(event => {
134
+ if (event.host) {
135
+ domains.add(event.host);
136
+ return;
137
+ }
138
+ try {
139
+ if (event.url) {
140
+ const parsed = new URL(event.url);
141
+ const host = String(parsed.host || "");
142
+ if (host) domains.add(host);
143
+ }
144
+ } catch {
145
+ // Ignore relative URLs
146
+ }
147
+ });
148
+ return Array.from(domains).filter(domain => domain && domain !== "").sort((a, b) => a.localeCompare(b));
149
+ }, [events]);
150
+ const availableUrls = useMemo(() => {
151
+ const urls = new Set();
152
+ events.forEach(event => {
153
+ if (!event.url) return;
154
+ try {
155
+ const parsed = new URL(event.url);
156
+ const origin = String(parsed.origin || "");
157
+ const pathname = String(parsed.pathname || "");
158
+ const normalized = `${origin}${pathname}`;
159
+ urls.add(normalized);
160
+ } catch {
161
+ // URL constructor fails for relative paths - fall back to raw value
162
+ urls.add(event.url);
163
+ }
164
+ });
165
+ return Array.from(urls).filter(url => url && url !== "").sort((a, b) => a.localeCompare(b));
166
+ }, [events]);
167
+ const suggestionItems = useMemo(() => {
168
+ const maxItemsPerGroup = 30;
169
+ const domainSuggestions = availableDomains.slice(0, maxItemsPerGroup);
170
+ const urlSuggestions = availableUrls.slice(0, maxItemsPerGroup);
171
+ return [...domainSuggestions, ...urlSuggestions];
172
+ }, [availableDomains, availableUrls]);
173
+ const handleDynamicFilterChange = useCallback((optionId, value) => {
174
+ const [group] = optionId.split("::");
175
+ if (group === "status") {
176
+ const nextStatus = value === "all" ? undefined : value;
177
+ onFilterChange({
178
+ ...filter,
179
+ status: nextStatus
180
+ });
181
+ return;
182
+ }
183
+ if (group === "method") {
184
+ const methodValue = String(value);
185
+ const currentMethods = filter.method || [];
186
+ const hasMethod = currentMethods.includes(methodValue);
187
+ const updatedMethods = hasMethod ? currentMethods.filter(method => method !== methodValue) : [methodValue];
188
+ onFilterChange({
189
+ ...filter,
190
+ method: updatedMethods.length > 0 ? updatedMethods : undefined
191
+ });
192
+ return;
193
+ }
194
+ if (group === "contentType") {
195
+ const typeValue = String(value);
196
+ const currentTypes = filter.contentType || [];
197
+ const hasType = currentTypes.includes(typeValue);
198
+ const updatedTypes = hasType ? currentTypes.filter(type => type !== typeValue) : [typeValue];
199
+ onFilterChange({
200
+ ...filter,
201
+ contentType: updatedTypes.length > 0 ? updatedTypes : undefined
202
+ });
203
+ }
204
+ }, [filter, onFilterChange]);
205
+ const filterSections = useMemo(() => {
206
+ const sections = [];
207
+ sections.push({
208
+ id: "status",
209
+ title: "Status",
210
+ type: "status",
211
+ data: ["all", "success", "error", "pending"].map(status => ({
212
+ id: `status::${status}`,
213
+ label: status.charAt(0).toUpperCase() + status.slice(1),
214
+ count: statusCounts[status],
215
+ icon: status === "success" ? CheckCircle : status === "error" ? XCircle : status === "pending" ? Clock : Globe,
216
+ color: status === "success" ? macOSColors.semantic.success : status === "error" ? macOSColors.semantic.error : status === "pending" ? macOSColors.semantic.warning : macOSColors.semantic.info,
217
+ isActive: filter.status === status || !filter.status && status === "all",
218
+ value: status
219
+ }))
220
+ });
221
+ const methodEntries = Object.entries(methodCounts);
222
+ if (methodEntries.length > 0) {
223
+ sections.push({
224
+ id: "method",
225
+ title: "Method",
226
+ type: "method",
227
+ data: methodEntries.map(([method, count]) => ({
228
+ id: `method::${method}`,
229
+ label: method,
230
+ count,
231
+ color: getMethodColor(method),
232
+ isActive: filter.method?.includes(method) ?? false,
233
+ value: method
234
+ }))
235
+ });
236
+ }
237
+ const contentTypeEntries = Object.entries(contentTypeCounts);
238
+ if (contentTypeEntries.length > 0) {
239
+ sections.push({
240
+ id: "contentType",
241
+ title: "Content Type",
242
+ type: "contentType",
243
+ data: contentTypeEntries.map(([type, count]) => {
244
+ const representativeEvent = events.find(event => getContentType(event).type === type);
245
+ const {
246
+ color
247
+ } = representativeEvent ? getContentType(representativeEvent) : {
248
+ color: macOSColors.text.muted
249
+ };
250
+ return {
251
+ id: `contentType::${type}`,
252
+ label: type,
253
+ count,
254
+ icon: getContentTypeIcon(type),
255
+ color,
256
+ isActive: filter.contentType?.includes(type) ?? false,
257
+ value: type
258
+ };
259
+ })
260
+ });
261
+ }
262
+ return sections;
263
+ }, [contentTypeCounts, events, filter.contentType, filter.method, filter.status, methodCounts, statusCounts]);
264
+ const dynamicFilterConfig = useMemo(() => ({
265
+ sections: filterSections,
266
+ addFilterSection: {
267
+ enabled: true,
268
+ placeholder: "Enter domain or URL pattern...",
269
+ title: "ACTIVE FILTERS",
270
+ icon: Filter
271
+ },
272
+ availableItemsSection: {
273
+ enabled: true,
274
+ title: "AVAILABLE DOMAINS & URLS",
275
+ emptyMessage: "No network events captured yet. Domains and URLs will appear here.",
276
+ items: suggestionItems
277
+ },
278
+ howItWorksSection: {
279
+ enabled: true,
280
+ title: "HOW NETWORK FILTERS WORK",
281
+ description: "Patterns hide matching requests from the network event list. Filters match if the domain or URL contains the provided text.",
282
+ examples: ["• example.com → filters any request whose host includes example.com", "• https://api.example.com/v1/users → filters that exact endpoint", "• /health → filters any URL path containing /health"],
283
+ icon: Filter
284
+ },
285
+ onFilterChange: handleDynamicFilterChange,
286
+ onPatternAdd: onAddPattern,
287
+ onPatternToggle: onTogglePattern,
288
+ activePatterns: ignoredPatterns
289
+ }), [filterSections, handleDynamicFilterChange, ignoredPatterns, onAddPattern, onTogglePattern, suggestionItems]);
290
+ return /*#__PURE__*/_jsx(DynamicFilterView, {
291
+ ...dynamicFilterConfig
292
+ });
293
+ }