@rozenite/network-activity-plugin 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (134) hide show
  1. package/README.md +3 -5
  2. package/dist/{panel.html → App.html} +3 -3
  3. package/dist/assets/App-CA1Fbh0I.js +25364 -0
  4. package/dist/assets/App-DoHQsY5s.css +1276 -0
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +8 -1
  8. package/dist/react-native.d.ts +1 -5
  9. package/dist/react-native.js +6 -171
  10. package/dist/rozenite.config.d.ts +7 -0
  11. package/dist/rozenite.json +1 -1
  12. package/dist/src/react-native/http/network-inspector.d.ts +8 -0
  13. package/dist/src/react-native/http/network-requests-registry.d.ts +6 -0
  14. package/dist/src/react-native/http/xhr-interceptor.d.ts +38 -0
  15. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  16. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  17. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  18. package/dist/src/react-native/sse/types.d.ts +6 -0
  19. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
  20. package/dist/src/react-native/utils.d.ts +6 -0
  21. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  22. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  23. package/dist/src/shared/client.d.ts +68 -0
  24. package/dist/src/shared/sse-events.d.ts +35 -0
  25. package/dist/src/shared/websocket-events.d.ts +60 -0
  26. package/dist/src/ui/App.d.ts +1 -0
  27. package/dist/src/ui/components/Badge.d.ts +9 -0
  28. package/dist/src/ui/components/Button.d.ts +11 -0
  29. package/dist/src/ui/components/Input.d.ts +3 -0
  30. package/dist/src/ui/components/JsonTree.d.ts +5 -0
  31. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  32. package/dist/src/ui/components/RequestList.d.ts +25 -0
  33. package/dist/src/ui/components/ScrollArea.d.ts +4 -0
  34. package/dist/src/ui/components/Separator.d.ts +3 -0
  35. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  36. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  37. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  38. package/dist/src/ui/state/derived.d.ts +5 -0
  39. package/dist/src/ui/state/hooks.d.ts +17 -0
  40. package/dist/src/ui/state/model.d.ts +98 -0
  41. package/dist/src/ui/state/store.d.ts +24 -0
  42. package/dist/src/ui/tabs/CookiesTab.d.ts +5 -0
  43. package/dist/src/ui/tabs/HeadersTab.d.ts +5 -0
  44. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  45. package/dist/src/ui/tabs/RequestTab.d.ts +5 -0
  46. package/dist/src/ui/tabs/ResponseTab.d.ts +6 -0
  47. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  48. package/dist/src/ui/tabs/TimingTab.d.ts +5 -0
  49. package/dist/src/ui/types.d.ts +26 -0
  50. package/dist/src/ui/utils/assert.d.ts +1 -0
  51. package/dist/src/ui/utils/cn.d.ts +2 -0
  52. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  53. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
  54. package/dist/src/ui/utils/getId.d.ts +1 -0
  55. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  56. package/dist/src/ui/views/InspectorView.d.ts +5 -0
  57. package/dist/src/ui/views/LoadingView.d.ts +1 -0
  58. package/dist/useNetworkActivityDevTools.cjs +759 -0
  59. package/dist/useNetworkActivityDevTools.js +757 -0
  60. package/package.json +31 -10
  61. package/postcss.config.js +6 -0
  62. package/project.json +12 -0
  63. package/react-native.ts +2 -1
  64. package/rozenite.config.ts +2 -2
  65. package/src/css-modules.d.ts +1 -1
  66. package/src/react-native/http/network-inspector.ts +226 -0
  67. package/src/react-native/http/network-requests-registry.ts +52 -0
  68. package/src/react-native/http/xhr-interceptor.ts +211 -0
  69. package/src/react-native/http/xml-request.d.ts +34 -0
  70. package/src/react-native/sse/event-source.ts +25 -0
  71. package/src/react-native/sse/sse-inspector.ts +117 -0
  72. package/src/react-native/sse/sse-interceptor.ts +162 -0
  73. package/src/react-native/sse/types.ts +9 -0
  74. package/src/react-native/useNetworkActivityDevTools.ts +73 -210
  75. package/src/react-native/utils.ts +43 -0
  76. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  77. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  78. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  79. package/src/shared/client.ts +86 -0
  80. package/src/shared/sse-events.ts +44 -0
  81. package/src/shared/websocket-events.ts +79 -0
  82. package/src/ui/App.tsx +19 -0
  83. package/src/ui/components/Badge.tsx +36 -0
  84. package/src/ui/components/Button.tsx +56 -0
  85. package/src/ui/components/Input.tsx +22 -0
  86. package/src/ui/components/JsonTree.tsx +50 -0
  87. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  88. package/src/ui/components/RequestList.tsx +295 -0
  89. package/src/ui/components/ScrollArea.tsx +48 -0
  90. package/src/ui/components/Separator.tsx +31 -0
  91. package/src/ui/components/SidePanel.tsx +323 -0
  92. package/src/ui/components/Tabs.tsx +55 -0
  93. package/src/ui/components/Toolbar.tsx +45 -0
  94. package/src/ui/globals.css +90 -0
  95. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  96. package/src/ui/state/derived.ts +112 -0
  97. package/src/ui/state/hooks.ts +44 -0
  98. package/src/ui/state/model.ts +129 -0
  99. package/src/ui/state/store.ts +559 -0
  100. package/src/ui/tabs/CookiesTab.tsx +279 -0
  101. package/src/ui/tabs/HeadersTab.tsx +110 -0
  102. package/src/ui/tabs/MessagesTab.tsx +276 -0
  103. package/src/ui/tabs/RequestTab.tsx +69 -0
  104. package/src/ui/tabs/ResponseTab.tsx +138 -0
  105. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  106. package/src/ui/tabs/TimingTab.tsx +60 -0
  107. package/src/ui/types.ts +34 -0
  108. package/src/ui/utils/assert.ts +5 -0
  109. package/src/ui/utils/cn.ts +6 -0
  110. package/src/ui/utils/copyToClipboard.ts +3 -0
  111. package/src/ui/utils/getHttpHeaderValue.ts +14 -0
  112. package/src/ui/utils/getId.ts +10 -0
  113. package/src/ui/utils/getStatusColor.ts +15 -0
  114. package/src/ui/views/InspectorView.tsx +53 -0
  115. package/src/ui/views/LoadingView.tsx +19 -0
  116. package/tailwind.config.ts +96 -0
  117. package/tsconfig.json +13 -6
  118. package/tsconfig.tsbuildinfo +1 -0
  119. package/vite.config.ts +13 -1
  120. package/dist/assets/panel-C5YgUUj5.js +0 -54
  121. package/dist/assets/panel-NCVczPb1.css +0 -1
  122. package/src/types/network.ts +0 -153
  123. package/src/ui/components.module.css +0 -158
  124. package/src/ui/components.tsx +0 -219
  125. package/src/ui/network-details.module.css +0 -57
  126. package/src/ui/network-details.tsx +0 -134
  127. package/src/ui/network-list.module.css +0 -122
  128. package/src/ui/network-list.tsx +0 -145
  129. package/src/ui/network-toolbar.module.css +0 -9
  130. package/src/ui/network-toolbar.tsx +0 -40
  131. package/src/ui/panel.module.css +0 -61
  132. package/src/ui/panel.tsx +0 -201
  133. package/src/ui/tanstack-query.tsx +0 -197
  134. package/src/ui/utils.ts +0 -89
package/src/ui/panel.tsx DELETED
@@ -1,201 +0,0 @@
1
- import React, { useState, useEffect, useMemo } from 'react';
2
- import { useRozeniteDevToolsClient } from "@rozenite/plugin-bridge";
3
- import { NetworkEventMap, NetworkEntry } from '../types/network';
4
- import styles from './panel.module.css';
5
- import { NetworkToolbar } from './network-toolbar';
6
- import { PanelHeader } from './components';
7
- import { NetworkList } from './network-list';
8
- import { NetworkDetails } from './network-details';
9
-
10
- export default function NetworkActivityPanel() {
11
- const client = useRozeniteDevToolsClient<NetworkEventMap>({
12
- pluginId: '@rozenite/network-activity-plugin',
13
- });
14
-
15
- const [networkEntries, setNetworkEntries] = useState<Map<string, NetworkEntry>>(new Map());
16
- const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
17
- const [isRecording, setIsRecording] = useState(true);
18
- const [containerHeight, setContainerHeight] = useState(0);
19
- const containerRef = React.useRef<HTMLDivElement>(null);
20
-
21
- // Convert Map to sorted array for rendering
22
- const sortedEntries = useMemo(() => {
23
- return Array.from(networkEntries.values())
24
- .sort((a, b) => b.startTime - a.startTime);
25
- }, [networkEntries]);
26
-
27
- const selectedEntry = selectedRequestId ? networkEntries.get(selectedRequestId) || null : null;
28
-
29
- useEffect(() => {
30
- if (!client) return;
31
- if (!isRecording) return;
32
-
33
- const subscriptions: Array<{ remove: () => void }> = [];
34
-
35
- // Subscribe to Network events
36
- subscriptions.push(
37
- client.onMessage('Network.requestWillBeSent', (payload) => {
38
- setNetworkEntries(prev => {
39
- const newMap = new Map(prev);
40
- newMap.set(payload.requestId, {
41
- requestId: payload.requestId,
42
- request: payload,
43
- status: 'pending',
44
- startTime: payload.timestamp,
45
- });
46
- return newMap;
47
- });
48
- })
49
- );
50
-
51
- subscriptions.push(
52
- client.onMessage('Network.requestWillBeSentExtraInfo', (payload) => {
53
- setNetworkEntries(prev => {
54
- const newMap = new Map(prev);
55
- const entry = newMap.get(payload.requestId);
56
- if (entry) {
57
- newMap.set(payload.requestId, {
58
- ...entry,
59
- extraInfo: payload,
60
- });
61
- }
62
- return newMap;
63
- });
64
- })
65
- );
66
-
67
- subscriptions.push(
68
- client.onMessage('Network.responseReceived', (payload) => {
69
- setNetworkEntries(prev => {
70
- const newMap = new Map(prev);
71
- const entry = newMap.get(payload.requestId);
72
- if (entry) {
73
- newMap.set(payload.requestId, {
74
- ...entry,
75
- response: payload,
76
- status: 'loading',
77
- });
78
- }
79
- return newMap;
80
- });
81
- })
82
- );
83
-
84
- subscriptions.push(
85
- client.onMessage('Network.loadingFinished', (payload) => {
86
- setNetworkEntries(prev => {
87
- const newMap = new Map(prev);
88
- const entry = newMap.get(payload.requestId);
89
- if (entry) {
90
- const endTime = payload.timestamp;
91
- const duration = (endTime - entry.startTime) * 1000; // Convert to milliseconds
92
- newMap.set(payload.requestId, {
93
- ...entry,
94
- loadingFinished: payload,
95
- status: 'finished',
96
- endTime,
97
- duration,
98
- });
99
- }
100
- return newMap;
101
- });
102
- })
103
- );
104
-
105
- subscriptions.push(
106
- client.onMessage('Network.loadingFailed', (payload) => {
107
- setNetworkEntries(prev => {
108
- const newMap = new Map(prev);
109
- const entry = newMap.get(payload.requestId);
110
- if (entry) {
111
- const endTime = payload.timestamp;
112
- const duration = (endTime - entry.startTime) * 1000; // Convert to milliseconds
113
- newMap.set(payload.requestId, {
114
- ...entry,
115
- loadingFailed: payload,
116
- status: 'failed',
117
- endTime,
118
- duration,
119
- });
120
- }
121
- return newMap;
122
- });
123
- })
124
- );
125
-
126
- return () => {
127
- subscriptions.forEach(sub => sub.remove());
128
- };
129
- }, [client, isRecording]);
130
-
131
- const clearNetworkLog = () => {
132
- setNetworkEntries(new Map());
133
- setSelectedRequestId(null);
134
- };
135
-
136
- const toggleRecording = () => {
137
- setIsRecording(!isRecording);
138
- };
139
-
140
- const handleSelectRequest = (requestId: string) => {
141
- setSelectedRequestId(requestId);
142
- };
143
-
144
- // Update container height on mount and resize
145
- useEffect(() => {
146
- const updateHeight = () => {
147
- if (containerRef.current) {
148
- const rect = containerRef.current.getBoundingClientRect();
149
- setContainerHeight(rect.height - 120); // Subtract toolbar and header height
150
- }
151
- };
152
-
153
- updateHeight();
154
- window.addEventListener('resize', updateHeight);
155
- return () => window.removeEventListener('resize', updateHeight);
156
- }, []);
157
-
158
- return (
159
- <div
160
- ref={containerRef}
161
- className={styles.container}
162
- >
163
- {/* Toolbar */}
164
- <NetworkToolbar
165
- isRecording={isRecording}
166
- onToggleRecording={toggleRecording}
167
- onClear={clearNetworkLog}
168
- requestCount={sortedEntries.length}
169
- />
170
-
171
- {/* Main Content */}
172
- <div className={styles.mainContent}>
173
- {/* Network List */}
174
- <div className={styles.networkListContainer}>
175
- {/* List Header */}
176
- <PanelHeader>
177
- <div className={styles.headerStatus}>Status</div>
178
- <div className={styles.headerMethod}>Method</div>
179
- <div className={styles.headerName}>Name</div>
180
- <div className={styles.headerTime}>Time</div>
181
- <div className={styles.headerSize}>Size</div>
182
- </PanelHeader>
183
-
184
- <div className={styles.listContent}>
185
- <NetworkList
186
- entries={sortedEntries}
187
- selectedRequestId={selectedRequestId}
188
- onSelect={handleSelectRequest}
189
- height={containerHeight}
190
- />
191
- </div>
192
- </div>
193
-
194
- {/* Details Panel */}
195
- <div className={styles.detailsContainer}>
196
- <NetworkDetails entry={selectedEntry} />
197
- </div>
198
- </div>
199
- </div>
200
- );
201
- }
@@ -1,197 +0,0 @@
1
- import { QueryCacheNotifyEvent, MutationCacheNotifyEvent, QueryClient, QueryClientProvider, Query, Mutation } from '@tanstack/react-query';
2
- import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools';
3
- import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
4
- import { useEffect, useRef } from 'react';
5
-
6
- const queryClient = new QueryClient({
7
- defaultOptions: {
8
- queries: {
9
- queryFn: async () => {
10
- // Prevent refetch from throwing an error
11
- return Promise.resolve(null);
12
- },
13
- },
14
- },
15
- });
16
-
17
- type DevToolsEventMap = {
18
- "DEVTOOLS_TO_DEVICE": unknown;
19
- "DEVICE_TO_DEVTOOLS": QueryCacheNotifyEvent | MutationCacheNotifyEvent;
20
- "DEVICE_TO_DEVTOOLS_ACK": { requestId: string; success: boolean };
21
- "DEVICE_TO_DEVTOOLS_INITIAL_DATA": { queries: Query[]; mutations: Mutation[] };
22
- "DEVTOOLS_TO_DEVICE_INITIAL_DATA_REQUEST": unknown;
23
- }
24
-
25
- const Wrapped = () => {
26
- const client = useRozeniteDevToolsClient<DevToolsEventMap>({
27
- pluginId: '@rozenite/tanstack-query-plugin',
28
- })
29
-
30
- // Track pending acknowledgments to prevent feedback loops
31
- const pendingAcknowledgment = useRef<Set<string>>(new Set());
32
-
33
- useEffect(() => {
34
- if (!client) return;
35
- client.send("DEVTOOLS_TO_DEVICE_INITIAL_DATA_REQUEST", null);
36
- }, [client]);
37
-
38
- useEffect(() => {
39
- if (!client) return;
40
-
41
- const handleEvent = (event: Event) => {
42
- const detail = (event as CustomEvent).detail;
43
-
44
- // Generate a unique request ID for this DevTools action
45
- const requestId = `devtools-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
46
-
47
- // Mark that we're waiting for acknowledgment
48
- pendingAcknowledgment.current.add(requestId);
49
-
50
- // Add the request ID to the event detail
51
- const eventWithRequestId = {
52
- ...detail,
53
- requestId,
54
- };
55
-
56
- client.send("DEVTOOLS_TO_DEVICE", eventWithRequestId);
57
- };
58
-
59
- window.addEventListener('@tanstack/query-devtools-event', handleEvent);
60
- return () => window.removeEventListener('@tanstack/query-devtools-event', handleEvent);
61
- }, [client])
62
-
63
- useEffect(() => {
64
- if (!client) return;
65
-
66
- const ackSubscription = client.onMessage("DEVICE_TO_DEVTOOLS_ACK", (ack) => {
67
- // Remove the request from pending acknowledgments
68
- pendingAcknowledgment.current.delete(ack.requestId);
69
- });
70
-
71
- const subscription = client.onMessage("DEVICE_TO_DEVTOOLS", (event) => {
72
- // Don't reflect events if we're waiting for acknowledgments
73
- if (pendingAcknowledgment.current.size > 0) {
74
- return;
75
- }
76
-
77
- if ('query' in event) {
78
- const { query, type } = event as QueryCacheNotifyEvent;
79
- const queryCache = queryClient.getQueryCache();
80
-
81
- if (type === 'updated') {
82
- const existingQuery = queryCache.get(query.queryHash);
83
- if (existingQuery) {
84
- existingQuery.setState(query.state);
85
- } else {
86
- queryCache.build(
87
- queryClient,
88
- {
89
- queryKey: query.queryKey,
90
- queryHash: query.queryHash,
91
- },
92
- query.state
93
- );
94
- }
95
- } else if (type === 'added') {
96
- const existingQuery = queryCache.get(query.queryHash);
97
- if (!existingQuery) {
98
- // Only add if it doesn't already exist
99
- queryCache.build(
100
- queryClient,
101
- {
102
- queryKey: query.queryKey,
103
- queryHash: query.queryHash,
104
- },
105
- query.state
106
- );
107
- }
108
- } else if (type === 'removed') {
109
- const existingQuery = queryCache.get(query.queryHash);
110
- if (existingQuery) {
111
- queryCache.remove(existingQuery);
112
- }
113
- }
114
- } else if ('mutation' in event) {
115
- const { mutation, type } = event as MutationCacheNotifyEvent;
116
- const mutationCache = queryClient.getMutationCache();
117
-
118
- if (type === 'added') {
119
- const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
120
- if (existingMutation) {
121
- mutationCache.remove(existingMutation);
122
- }
123
-
124
- mutationCache.build(
125
- queryClient,
126
- mutation.options,
127
- mutation.state
128
- );
129
- } else if (type === 'removed') {
130
- const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
131
- if (existingMutation) {
132
- mutationCache.remove(existingMutation);
133
- }
134
- } else if (type === 'updated') {
135
- const existingMutation = mutationCache.find({ mutationKey: mutation.options.mutationKey });
136
-
137
- if (existingMutation) {
138
- mutationCache.remove(existingMutation);
139
- mutationCache.build(
140
- queryClient,
141
- mutation.options,
142
- mutation.state
143
- );
144
- }
145
- }
146
- }
147
- })
148
-
149
- const initialDataSubscription = client.onMessage("DEVICE_TO_DEVTOOLS_INITIAL_DATA", (event) => {
150
- // Clear existing data first
151
- queryClient.clear();
152
- queryClient.getMutationCache().clear();
153
-
154
- // Restore queries
155
- const queryCache = queryClient.getQueryCache();
156
- event.queries.forEach(query => {
157
- queryCache.build(
158
- queryClient,
159
- {
160
- queryKey: query.queryKey,
161
- queryHash: query.queryHash,
162
- },
163
- query.state
164
- );
165
- });
166
-
167
- // Restore mutations
168
- const mutationCache = queryClient.getMutationCache();
169
- event.mutations.forEach(mutation => {
170
- mutationCache.build(
171
- queryClient,
172
- mutation.options,
173
- mutation.state
174
- );
175
- });
176
- });
177
-
178
- return () => {
179
- subscription.remove();
180
- ackSubscription.remove();
181
- initialDataSubscription.remove();
182
- };
183
- }, [client, queryClient])
184
-
185
-
186
- return (
187
- <ReactQueryDevtoolsPanel />
188
- )
189
- }
190
-
191
- export default function TanStackQueryPanel() {
192
- return (
193
- <QueryClientProvider client={queryClient}>
194
- <Wrapped />
195
- </QueryClientProvider>
196
- )
197
- }
package/src/ui/utils.ts DELETED
@@ -1,89 +0,0 @@
1
- // Utility functions for formatting and styling
2
-
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': return '#61affe';
24
- case 'POST': return '#49cc90';
25
- case 'PUT': return '#fca130';
26
- case 'DELETE': return '#f93e3e';
27
- case 'PATCH': return '#50e3c2';
28
- default: return '#757575';
29
- }
30
- };
31
-
32
- export const formatFileSize = (bytes: number): string => {
33
- if (bytes === 0) return '0 B';
34
- const k = 1024;
35
- const sizes = ['B', 'KB', 'MB', 'GB'];
36
- const i = Math.floor(Math.log(bytes) / Math.log(k));
37
- return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
38
- };
39
-
40
- export const parseUrl = (url: string) => {
41
- try {
42
- const urlObj = new URL(url);
43
- return {
44
- domain: urlObj.hostname,
45
- path: urlObj.pathname + urlObj.search,
46
- protocol: urlObj.protocol,
47
- };
48
- } catch {
49
- return {
50
- domain: 'Invalid URL',
51
- path: url,
52
- protocol: 'unknown',
53
- };
54
- }
55
- };
56
-
57
- export const truncateText = (text: string, maxLength: number): string => {
58
- if (text.length <= maxLength) return text;
59
- return text.substring(0, maxLength) + '...';
60
- };
61
-
62
- export const formatLongUrl = (url: string, maxLength = 80): string => {
63
- if (url.length <= maxLength) return url;
64
-
65
- try {
66
- const urlObj = new URL(url);
67
- const protocol = urlObj.protocol;
68
- const hostname = urlObj.hostname;
69
- const pathname = urlObj.pathname;
70
- const search = urlObj.search;
71
-
72
- // Keep protocol and hostname, truncate path if needed
73
- const baseLength = protocol.length + hostname.length + 3; // +3 for "://"
74
- const remainingLength = maxLength - baseLength - 3; // -3 for "..."
75
-
76
- if (remainingLength <= 0) {
77
- return `${protocol}//${hostname}...`;
78
- }
79
-
80
- const fullPath = pathname + search;
81
- if (fullPath.length <= remainingLength) {
82
- return url;
83
- }
84
-
85
- return `${protocol}//${hostname}${fullPath.substring(0, remainingLength)}...`;
86
- } catch {
87
- return truncateText(url, maxLength);
88
- }
89
- };