@rozenite/network-activity-plugin 1.0.0-alpha.5 → 1.0.0-alpha.6

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 (75) hide show
  1. package/dist/{panel.html → App.html} +3 -3
  2. package/dist/assets/App-CIflVb88.js +24164 -0
  3. package/dist/assets/App-Czu6Vt2P.css +1233 -0
  4. package/dist/react-native.cjs +1 -1
  5. package/dist/react-native.d.ts +1 -90
  6. package/dist/rozenite.config.d.ts +7 -0
  7. package/dist/rozenite.json +1 -1
  8. package/dist/src/react-native/network-inspector.d.ts +8 -0
  9. package/dist/src/react-native/network-requests-registry.d.ts +6 -0
  10. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
  11. package/dist/src/react-native/xhr-interceptor.d.ts +38 -0
  12. package/dist/src/shared/client.d.ts +64 -0
  13. package/dist/src/ui/App.d.ts +1 -0
  14. package/dist/src/ui/components/Badge.d.ts +9 -0
  15. package/dist/src/ui/components/Button.d.ts +11 -0
  16. package/dist/src/ui/components/Input.d.ts +3 -0
  17. package/dist/src/ui/components/JsonTree.d.ts +5 -0
  18. package/dist/src/ui/components/RequestList.d.ts +45 -0
  19. package/dist/src/ui/components/ScrollArea.d.ts +4 -0
  20. package/dist/src/ui/components/Separator.d.ts +3 -0
  21. package/dist/src/ui/tabs/CookiesTab.d.ts +8 -0
  22. package/dist/src/ui/tabs/HeadersTab.d.ts +17 -0
  23. package/dist/src/ui/tabs/RequestTab.d.ts +10 -0
  24. package/dist/src/ui/tabs/ResponseTab.d.ts +12 -0
  25. package/dist/src/ui/tabs/TimingTab.d.ts +7 -0
  26. package/dist/src/ui/types.d.ts +23 -0
  27. package/dist/src/ui/utils.d.ts +2 -0
  28. package/dist/src/ui/views/InspectorView.d.ts +5 -0
  29. package/dist/src/ui/views/LoadingView.d.ts +1 -0
  30. package/dist/useNetworkActivityDevTools.cjs +360 -0
  31. package/dist/useNetworkActivityDevTools.js +108 -236
  32. package/package.json +23 -16
  33. package/postcss.config.js +6 -0
  34. package/rozenite.config.ts +1 -1
  35. package/src/react-native/network-inspector.ts +113 -260
  36. package/src/react-native/network-requests-registry.ts +7 -77
  37. package/src/react-native/useNetworkActivityDevTools.ts +1 -1
  38. package/src/react-native/xhr-interceptor.ts +2 -2
  39. package/src/react-native/xml-request.d.ts +11 -1
  40. package/src/shared/client.ts +80 -0
  41. package/src/ui/App.tsx +19 -0
  42. package/src/ui/components/Badge.tsx +36 -0
  43. package/src/ui/components/Button.tsx +56 -0
  44. package/src/ui/components/Input.tsx +22 -0
  45. package/src/ui/components/JsonTree.tsx +37 -0
  46. package/src/ui/components/RequestList.tsx +376 -0
  47. package/src/ui/components/ScrollArea.tsx +48 -0
  48. package/src/ui/components/Separator.tsx +31 -0
  49. package/src/ui/components/Tabs.tsx +55 -0
  50. package/src/ui/globals.css +90 -0
  51. package/src/ui/tabs/CookiesTab.tsx +290 -0
  52. package/src/ui/tabs/HeadersTab.tsx +117 -0
  53. package/src/ui/tabs/RequestTab.tsx +72 -0
  54. package/src/ui/tabs/ResponseTab.tsx +140 -0
  55. package/src/ui/tabs/TimingTab.tsx +71 -0
  56. package/src/ui/types.ts +30 -0
  57. package/src/ui/utils.ts +5 -97
  58. package/src/ui/views/InspectorView.tsx +349 -0
  59. package/src/ui/views/LoadingView.tsx +19 -0
  60. package/tailwind.config.ts +93 -0
  61. package/dist/assets/panel-BNxB_KsS.js +0 -16663
  62. package/dist/assets/panel-DXGMsavf.css +0 -555
  63. package/src/types/client.ts +0 -111
  64. package/src/types/network.ts +0 -32
  65. package/src/ui/components.module.css +0 -158
  66. package/src/ui/components.tsx +0 -241
  67. package/src/ui/network-details.module.css +0 -197
  68. package/src/ui/network-details.tsx +0 -345
  69. package/src/ui/network-list.module.css +0 -128
  70. package/src/ui/network-list.tsx +0 -240
  71. package/src/ui/network-toolbar.module.css +0 -9
  72. package/src/ui/network-toolbar.tsx +0 -34
  73. package/src/ui/panel.module.css +0 -67
  74. package/src/ui/panel.tsx +0 -318
  75. package/src/ui/tanstack-query.tsx +0 -204
@@ -0,0 +1,140 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { ScrollArea } from '../components/ScrollArea';
3
+ import { JsonTree } from '../components/JsonTree';
4
+
5
+ export type ResponseTabProps = {
6
+ selectedRequest: {
7
+ id: string;
8
+ type: string;
9
+ responseBody?: {
10
+ type: string;
11
+ data: string | null;
12
+ };
13
+ };
14
+ onRequestResponseBody: (requestId: string) => void;
15
+ };
16
+
17
+ export const ResponseTab = ({
18
+ selectedRequest,
19
+ onRequestResponseBody,
20
+ }: ResponseTabProps) => {
21
+ const onRequestResponseBodyRef = useRef(onRequestResponseBody);
22
+
23
+ useEffect(() => {
24
+ onRequestResponseBodyRef.current = onRequestResponseBody;
25
+ }, [onRequestResponseBody]);
26
+
27
+ useEffect(() => {
28
+ if (onRequestResponseBodyRef.current) {
29
+ onRequestResponseBodyRef.current(selectedRequest.id);
30
+ }
31
+ }, [selectedRequest.id]);
32
+
33
+ const renderResponseBody = () => {
34
+ if (!selectedRequest?.responseBody) {
35
+ return (
36
+ <div className="text-sm text-gray-400">
37
+ No response body available for this request
38
+ </div>
39
+ );
40
+ }
41
+
42
+ const { type, data } = selectedRequest.responseBody;
43
+
44
+ // Handle null data
45
+ if (data === null) {
46
+ return (
47
+ <div className="text-sm text-gray-400">
48
+ No response body available for this request
49
+ </div>
50
+ );
51
+ }
52
+
53
+ // Handle JSON content
54
+ if (type === 'application/json') {
55
+ try {
56
+ const jsonData = JSON.parse(data);
57
+ return (
58
+ <div className="space-y-4">
59
+ <div className="text-sm mb-2">
60
+ <span className="text-gray-400">Content-Type: </span>
61
+ <span className="text-blue-400">{type}</span>
62
+ </div>
63
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
64
+ <JsonTree data={jsonData} />
65
+ </div>
66
+ </div>
67
+ );
68
+ } catch {
69
+ // Fallback to pre tag if JSON parsing fails
70
+ return (
71
+ <div className="space-y-4">
72
+ <div className="text-sm mb-2">
73
+ <span className="text-gray-400">Content-Type: </span>
74
+ <span className="text-blue-400">{type}</span>
75
+ </div>
76
+ <pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
77
+ {data}
78
+ </pre>
79
+ <div className="text-xs text-gray-500">
80
+ ⚠️ Failed to parse as JSON, showing as raw text
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+ }
86
+
87
+ // Handle HTML content
88
+ if (type === 'text/html') {
89
+ return (
90
+ <div className="space-y-4">
91
+ <div className="text-sm mb-2">
92
+ <span className="text-gray-400">Content-Type: </span>
93
+ <span className="text-blue-400">{type}</span>
94
+ </div>
95
+ <pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
96
+ {data}
97
+ </pre>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ // Handle other text content types
103
+ if (
104
+ type.startsWith('text/') ||
105
+ type === 'application/xml' ||
106
+ type === 'application/javascript'
107
+ ) {
108
+ return (
109
+ <div className="space-y-4">
110
+ <div className="text-sm mb-2">
111
+ <span className="text-gray-400">Content-Type: </span>
112
+ <span className="text-blue-400">{type}</span>
113
+ </div>
114
+ <pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
115
+ {data}
116
+ </pre>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ // Handle other content types
122
+ return (
123
+ <div className="space-y-4">
124
+ <div className="text-sm mb-2">
125
+ <span className="text-gray-400">Content-Type: </span>
126
+ <span className="text-blue-400">{type}</span>
127
+ </div>
128
+ <div className="text-sm text-gray-400">
129
+ Binary content not shown - {data.length} bytes
130
+ </div>
131
+ </div>
132
+ );
133
+ };
134
+
135
+ return (
136
+ <ScrollArea className="h-full min-h-0 p-4">
137
+ {renderResponseBody()}
138
+ </ScrollArea>
139
+ );
140
+ };
@@ -0,0 +1,71 @@
1
+ import { ScrollArea } from '../components/ScrollArea';
2
+ import { NetworkRequest } from '../components/RequestList';
3
+ import { NetworkEntry } from '../types';
4
+
5
+ export type TimingTabProps = {
6
+ selectedRequest: NetworkRequest | null;
7
+ networkEntries: Map<string, NetworkEntry>;
8
+ };
9
+
10
+ export const TimingTab = ({
11
+ selectedRequest,
12
+ networkEntries,
13
+ }: TimingTabProps) => {
14
+ const networkEntry = selectedRequest
15
+ ? networkEntries.get(selectedRequest.id)
16
+ : null;
17
+
18
+ if (!selectedRequest || !networkEntry) {
19
+ return (
20
+ <div className="flex items-center justify-center h-full text-gray-400">
21
+ Select a request to view timing information
22
+ </div>
23
+ );
24
+ }
25
+
26
+ const startTime = networkEntry.startTime || 0;
27
+ const endTime = networkEntry.endTime || 0;
28
+ const ttfb = networkEntry.ttfb || 0;
29
+ const duration = networkEntry.duration || 0;
30
+
31
+ const formatTime = (time: number): string => {
32
+ if (time < 1) {
33
+ return `${Math.round(time * 1000)} μs`;
34
+ }
35
+ return `${time.toFixed(1)} ms`;
36
+ };
37
+
38
+ const formatTimestamp = (timestamp: number): string => {
39
+ return new Date(timestamp * 1000).toLocaleTimeString();
40
+ };
41
+
42
+ return (
43
+ <ScrollArea className="h-full p-4">
44
+ <div className="space-y-4">
45
+ <div className="space-y-2">
46
+ <div className="flex justify-between text-sm">
47
+ <span className="text-gray-400">Start Time</span>
48
+ <span className="text-gray-300">{formatTimestamp(startTime)}</span>
49
+ </div>
50
+ <div className="flex justify-between text-sm">
51
+ <span className="text-gray-400">Time To First Byte (TTFB)</span>
52
+ <span className="text-gray-300">{formatTime(ttfb)}</span>
53
+ </div>
54
+ <div className="flex justify-between text-sm">
55
+ <span className="text-gray-400">End Time</span>
56
+ <span className="text-gray-300">
57
+ {endTime ? formatTimestamp(endTime) : 'Pending'}
58
+ </span>
59
+ </div>
60
+ </div>
61
+
62
+ <div className="border-t border-gray-700 pt-4">
63
+ <div className="flex justify-between text-sm font-medium">
64
+ <span className="text-gray-300">Total Duration</span>
65
+ <span className="text-gray-100">{formatTime(duration)}</span>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </ScrollArea>
70
+ );
71
+ };
@@ -0,0 +1,30 @@
1
+ import {
2
+ RequestId,
3
+ Request,
4
+ Response,
5
+ Initiator,
6
+ ResourceType,
7
+ } from '../shared/client';
8
+
9
+ export type NetworkEntry = {
10
+ requestId: RequestId;
11
+ url: string;
12
+ method: string;
13
+ headers: Record<string, string>;
14
+ postData?: string;
15
+ status: 'pending' | 'loading' | 'finished' | 'failed';
16
+ startTime: number;
17
+ endTime?: number;
18
+ duration?: number;
19
+ ttfb?: number;
20
+ type?: ResourceType;
21
+ initiator?: Initiator;
22
+ request?: Request;
23
+ response?: Response;
24
+ responseBody?: {
25
+ body: string | null;
26
+ };
27
+ error?: string;
28
+ canceled?: boolean;
29
+ size?: number;
30
+ };
package/src/ui/utils.ts CHANGED
@@ -1,98 +1,6 @@
1
- // Utility functions for formatting and styling
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
2
3
 
3
- export const formatTime = (timestamp: number): string => {
4
- const date = new Date(timestamp * 1000);
5
- return date.toLocaleTimeString();
6
- };
7
-
8
- export const formatDuration = (duration: number): string => {
9
- if (duration < 1000) return `${Math.round(duration)}ms`;
10
- return `${(duration / 1000).toFixed(2)}s`;
11
- };
12
-
13
- export const getStatusColor = (status: number): string => {
14
- if (status >= 200 && status < 300) return '#4caf50';
15
- if (status >= 300 && status < 400) return '#ff9800';
16
- if (status >= 400 && status < 500) return '#f44336';
17
- if (status >= 500) return '#9c27b0';
18
- return '#757575';
19
- };
20
-
21
- export const getMethodColor = (method: string): string => {
22
- switch (method.toUpperCase()) {
23
- case 'GET':
24
- return '#61affe';
25
- case 'POST':
26
- return '#49cc90';
27
- case 'PUT':
28
- return '#fca130';
29
- case 'DELETE':
30
- return '#f93e3e';
31
- case 'PATCH':
32
- return '#50e3c2';
33
- default:
34
- return '#757575';
35
- }
36
- };
37
-
38
- export const formatFileSize = (bytes: number): string => {
39
- if (bytes === 0) return '0 B';
40
- const k = 1024;
41
- const sizes = ['B', 'KB', 'MB', 'GB'];
42
- const i = Math.floor(Math.log(bytes) / Math.log(k));
43
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
44
- };
45
-
46
- export const parseUrl = (url: string) => {
47
- try {
48
- const urlObj = new URL(url);
49
- return {
50
- domain: urlObj.hostname,
51
- path: urlObj.pathname + urlObj.search,
52
- protocol: urlObj.protocol,
53
- };
54
- } catch {
55
- return {
56
- domain: 'Invalid URL',
57
- path: url,
58
- protocol: 'unknown',
59
- };
60
- }
61
- };
62
-
63
- export const truncateText = (text: string, maxLength: number): string => {
64
- if (text.length <= maxLength) return text;
65
- return text.substring(0, maxLength) + '...';
66
- };
67
-
68
- export const formatLongUrl = (url: string, maxLength = 80): string => {
69
- if (url.length <= maxLength) return url;
70
-
71
- try {
72
- const urlObj = new URL(url);
73
- const protocol = urlObj.protocol;
74
- const hostname = urlObj.hostname;
75
- const pathname = urlObj.pathname;
76
- const search = urlObj.search;
77
-
78
- // Keep protocol and hostname, truncate path if needed
79
- const baseLength = protocol.length + hostname.length + 3; // +3 for "://"
80
- const remainingLength = maxLength - baseLength - 3; // -3 for "..."
81
-
82
- if (remainingLength <= 0) {
83
- return `${protocol}//${hostname}...`;
84
- }
85
-
86
- const fullPath = pathname + search;
87
- if (fullPath.length <= remainingLength) {
88
- return url;
89
- }
90
-
91
- return `${protocol}//${hostname}${fullPath.substring(
92
- 0,
93
- remainingLength
94
- )}...`;
95
- } catch {
96
- return truncateText(url, maxLength);
97
- }
98
- };
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,349 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { Badge } from '../components/Badge';
3
+ import { Button } from '../components/Button';
4
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/Tabs';
5
+ import { HeadersTab } from '../tabs/HeadersTab';
6
+ import { RequestTab } from '../tabs/RequestTab';
7
+ import { ResponseTab } from '../tabs/ResponseTab';
8
+ import { CookiesTab } from '../tabs/CookiesTab';
9
+ import { TimingTab } from '../tabs/TimingTab';
10
+ import {
11
+ RequestList,
12
+ processNetworkEntries,
13
+ getTypeColor,
14
+ getStatusColor,
15
+ } from '../components/RequestList';
16
+ import { Circle, Square, Trash2, X } from 'lucide-react';
17
+ import {
18
+ NetworkActivityDevToolsClient,
19
+ NetworkActivityEventMap,
20
+ RequestId,
21
+ } from '../../shared/client';
22
+ import { NetworkEntry } from '../types';
23
+
24
+ export type InspectorViewProps = {
25
+ client: NetworkActivityDevToolsClient;
26
+ };
27
+
28
+ export const InspectorView = ({ client }: InspectorViewProps) => {
29
+ const [isRecording, setIsRecording] = useState(true);
30
+ const [selectedRequestId, setSelectedRequestId] = useState<RequestId | null>(
31
+ null
32
+ );
33
+ const [networkEntries, setNetworkEntries] = useState<
34
+ Map<RequestId, NetworkEntry>
35
+ >(new Map());
36
+
37
+ const selectedRequest = useMemo(() => {
38
+ if (!selectedRequestId) return null;
39
+ const processedRequests = processNetworkEntries(networkEntries);
40
+ return (
41
+ processedRequests.find((request) => request.id === selectedRequestId) ||
42
+ null
43
+ );
44
+ }, [selectedRequestId, networkEntries]);
45
+
46
+ useEffect(() => {
47
+ const handleRequestSent = (
48
+ data: NetworkActivityEventMap['request-sent']
49
+ ) => {
50
+ const entry: NetworkEntry = {
51
+ requestId: data.requestId,
52
+ url: data.request.url,
53
+ method: data.request.method,
54
+ headers: data.request.headers,
55
+ postData: data.request.postData,
56
+ status: 'pending',
57
+ startTime: data.timestamp,
58
+ type: data.type,
59
+ initiator: data.initiator,
60
+ request: data.request,
61
+ };
62
+
63
+ setNetworkEntries((prev) => new Map(prev).set(data.requestId, entry));
64
+ };
65
+
66
+ const handleResponseReceived = (
67
+ data: NetworkActivityEventMap['response-received']
68
+ ) => {
69
+ setNetworkEntries((prev) => {
70
+ const entry = prev.get(data.requestId);
71
+ if (!entry) return prev;
72
+
73
+ const updatedEntry: NetworkEntry = {
74
+ ...entry,
75
+ status: 'loading',
76
+ response: data.response,
77
+ };
78
+
79
+ return new Map(prev).set(data.requestId, updatedEntry);
80
+ });
81
+ };
82
+
83
+ const handleRequestCompleted = (
84
+ data: NetworkActivityEventMap['request-completed']
85
+ ) => {
86
+ setNetworkEntries((prev) => {
87
+ const entry = prev.get(data.requestId);
88
+ if (!entry) return prev;
89
+
90
+ const updatedEntry: NetworkEntry = {
91
+ ...entry,
92
+ status: 'finished',
93
+ endTime: data.timestamp,
94
+ duration: data.duration,
95
+ ttfb: data.ttfb,
96
+ size: data.size,
97
+ };
98
+
99
+ return new Map(prev).set(data.requestId, updatedEntry);
100
+ });
101
+ };
102
+
103
+ const handleRequestFailed = (
104
+ data: NetworkActivityEventMap['request-failed']
105
+ ) => {
106
+ setNetworkEntries((prev) => {
107
+ const entry = prev.get(data.requestId);
108
+ if (!entry) return prev;
109
+
110
+ const updatedEntry: NetworkEntry = {
111
+ ...entry,
112
+ status: 'failed',
113
+ error: data.error,
114
+ canceled: data.canceled,
115
+ };
116
+
117
+ return new Map(prev).set(data.requestId, updatedEntry);
118
+ });
119
+ };
120
+
121
+ // Subscribe to network events
122
+ const unsubscribeRequestSent = client.onMessage(
123
+ 'request-sent',
124
+ handleRequestSent
125
+ );
126
+ const unsubscribeResponseReceived = client.onMessage(
127
+ 'response-received',
128
+ handleResponseReceived
129
+ );
130
+ const unsubscribeRequestCompleted = client.onMessage(
131
+ 'request-completed',
132
+ handleRequestCompleted
133
+ );
134
+ const unsubscribeRequestFailed = client.onMessage(
135
+ 'request-failed',
136
+ handleRequestFailed
137
+ );
138
+
139
+ const handleResponseBody = (
140
+ data: NetworkActivityEventMap['response-body']
141
+ ) => {
142
+ setNetworkEntries((prev) => {
143
+ const entry = prev.get(data.requestId);
144
+ if (!entry) return prev;
145
+
146
+ const updatedEntry: NetworkEntry = {
147
+ ...entry,
148
+ responseBody: {
149
+ ...entry.responseBody,
150
+ body: data.body,
151
+ },
152
+ };
153
+
154
+ console.log(updatedEntry);
155
+ return new Map(prev).set(data.requestId, updatedEntry);
156
+ });
157
+ };
158
+
159
+ const unsubscribeResponseBody = client.onMessage(
160
+ 'response-body',
161
+ handleResponseBody
162
+ );
163
+
164
+ return () => {
165
+ unsubscribeRequestSent.remove();
166
+ unsubscribeResponseReceived.remove();
167
+ unsubscribeRequestCompleted.remove();
168
+ unsubscribeRequestFailed.remove();
169
+ unsubscribeResponseBody.remove();
170
+ };
171
+ }, [client]);
172
+
173
+ useEffect(() => {
174
+ if (isRecording) {
175
+ client.send('network-enable', {});
176
+
177
+ return () => {
178
+ client.send('network-disable', {});
179
+ };
180
+ }
181
+ }, [isRecording, client]);
182
+
183
+ const clearRequests = () => {
184
+ setNetworkEntries(new Map());
185
+ setSelectedRequestId(null);
186
+ };
187
+
188
+ const closeSidePanel = () => {
189
+ setSelectedRequestId(null);
190
+ };
191
+
192
+ return (
193
+ <div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
194
+ {/* Toolbar */}
195
+ <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
196
+ <Button
197
+ variant="ghost"
198
+ size="sm"
199
+ onClick={() => setIsRecording(!isRecording)}
200
+ className={`h-8 w-8 p-0 ${
201
+ isRecording
202
+ ? 'text-red-400 hover:text-red-300'
203
+ : 'text-gray-400 hover:text-blue-400'
204
+ }`}
205
+ >
206
+ {isRecording ? (
207
+ <Circle className="h-4 w-4 fill-current" />
208
+ ) : (
209
+ <Square className="h-4 w-4" />
210
+ )}
211
+ </Button>
212
+ <Button
213
+ variant="ghost"
214
+ size="sm"
215
+ onClick={clearRequests}
216
+ className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
217
+ >
218
+ <Trash2 className="h-4 w-4" />
219
+ </Button>
220
+ </div>
221
+
222
+ <div className="flex flex-1 overflow-hidden">
223
+ {/* Request List */}
224
+ <div
225
+ className={`flex flex-col ${
226
+ selectedRequest ? 'w-1/2' : 'w-full'
227
+ } border-r border-gray-700 overflow-hidden`}
228
+ >
229
+ <RequestList
230
+ networkEntries={networkEntries}
231
+ selectedRequestId={selectedRequestId}
232
+ onRequestSelect={setSelectedRequestId}
233
+ />
234
+ </div>
235
+
236
+ {/* Side Panel */}
237
+ {selectedRequest && (
238
+ <div className="w-1/2 flex flex-col bg-gray-900">
239
+ {/* Side Panel Header */}
240
+ <div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800">
241
+ <div className="flex items-center gap-2">
242
+ <div
243
+ className={`w-3 h-3 rounded-full ${getTypeColor(
244
+ selectedRequest.type
245
+ )}`}
246
+ ></div>
247
+ <span className="font-medium">{selectedRequest.name}</span>
248
+ <Badge
249
+ variant="outline"
250
+ className={`${getStatusColor(
251
+ selectedRequest.status
252
+ )} border-current`}
253
+ >
254
+ {selectedRequest.status}
255
+ </Badge>
256
+ </div>
257
+ <Button
258
+ variant="ghost"
259
+ size="sm"
260
+ onClick={closeSidePanel}
261
+ className="h-6 w-6 p-0 text-gray-400 hover:text-blue-400"
262
+ >
263
+ <X className="h-4 w-4" />
264
+ </Button>
265
+ </div>
266
+
267
+ {/* Side Panel Content */}
268
+ <div className="flex-1 overflow-hidden">
269
+ <Tabs defaultValue="headers" className="h-full flex flex-col">
270
+ <TabsList className="grid w-full grid-cols-5 bg-gray-800 rounded-none border-b border-gray-700">
271
+ <TabsTrigger
272
+ value="headers"
273
+ className="data-[state=active]:bg-gray-700"
274
+ >
275
+ Headers
276
+ </TabsTrigger>
277
+ <TabsTrigger
278
+ value="request"
279
+ className="data-[state=active]:bg-gray-700"
280
+ >
281
+ Request
282
+ </TabsTrigger>
283
+ <TabsTrigger
284
+ value="response"
285
+ className="data-[state=active]:bg-gray-700"
286
+ >
287
+ Response
288
+ </TabsTrigger>
289
+ <TabsTrigger
290
+ value="cookies"
291
+ className="data-[state=active]:bg-gray-700"
292
+ >
293
+ Cookies
294
+ </TabsTrigger>
295
+ <TabsTrigger
296
+ value="timing"
297
+ className="data-[state=active]:bg-gray-700"
298
+ >
299
+ Timing
300
+ </TabsTrigger>
301
+ </TabsList>
302
+
303
+ <TabsContent
304
+ value="headers"
305
+ className="flex-1 m-0 overflow-hidden"
306
+ >
307
+ <HeadersTab
308
+ selectedRequest={selectedRequest}
309
+ networkEntries={networkEntries}
310
+ getStatusColor={getStatusColor}
311
+ />
312
+ </TabsContent>
313
+
314
+ <TabsContent value="request" className="flex-1 m-0">
315
+ <RequestTab selectedRequest={selectedRequest} />
316
+ </TabsContent>
317
+
318
+ <TabsContent value="response" className="flex-1 m-0">
319
+ <ResponseTab
320
+ selectedRequest={selectedRequest}
321
+ onRequestResponseBody={(requestId) => {
322
+ client.send('get-response-body', {
323
+ requestId,
324
+ });
325
+ }}
326
+ />
327
+ </TabsContent>
328
+
329
+ <TabsContent value="cookies" className="flex-1 m-0">
330
+ <CookiesTab
331
+ selectedRequest={selectedRequest}
332
+ networkEntries={networkEntries}
333
+ />
334
+ </TabsContent>
335
+
336
+ <TabsContent value="timing" className="flex-1 m-0">
337
+ <TimingTab
338
+ selectedRequest={selectedRequest}
339
+ networkEntries={networkEntries}
340
+ />
341
+ </TabsContent>
342
+ </Tabs>
343
+ </div>
344
+ </div>
345
+ )}
346
+ </div>
347
+ </div>
348
+ );
349
+ };