reactoradar 1.6.4 → 1.6.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.
@@ -1,559 +0,0 @@
1
- 'use strict';
2
-
3
- const state = window.RNDebug.state;
4
- const $ = window.RNDebug.$;
5
- const esc = window.RNDebug.esc;
6
- const ts = window.RNDebug.ts;
7
- const tryURL = window.RNDebug.tryURL;
8
- const formatSize = window.RNDebug.formatSize;
9
- const renderJSON = window.RNDebug.renderJSON;
10
- const showContextMenu = window.RNDebug.showContextMenu;
11
- const createTreeNode = window.RNDebug.createTreeNode;
12
- const addConsoleLog = window.RNDebug.addConsoleLog;
13
-
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // NETWORK PANEL (Chrome DevTools-style)
16
- // ─────────────────────────────────────────────────────────────────────────────
17
- const NET_COLS = [
18
- { key: 'name', label: 'Name', width: 260, min: 100 },
19
- { key: 'status', label: 'Status', width: 60, min: 40 },
20
- { key: 'type', label: 'Type', width: 70, min: 40 },
21
- { key: 'initiator', label: 'Initiator', width: 90, min: 50 },
22
- { key: 'size', label: 'Size', width: 70, min: 40 },
23
- { key: 'time', label: 'Time', width: 70, min: 40 },
24
- { key: 'waterfall', label: 'Waterfall', width: 120, min: 60 },
25
- ];
26
-
27
- function initNetworkPanel() {
28
- const panel = $('panel-network');
29
- panel.innerHTML = `
30
- <div class="panel-toolbar">
31
- <span class="panel-label">Network</span>
32
- <span class="badge" id="nBadge">0</span>
33
- <div class="ml-auto" style="display:flex;align-items:center;gap:6px">
34
- <label class="toggle-label" for="netToggle">
35
- <span class="toggle-text" id="netToggleText">Capture ON</span>
36
- <input type="checkbox" id="netToggle" class="toggle-input" checked />
37
- <span class="toggle-slider"></span>
38
- </label>
39
- </div>
40
- </div>
41
- <div class="net-filter-bar" id="netFilterBar">
42
- <input id="netSearchInput" class="net-search-input" placeholder="Filter URLs..." />
43
- <div class="net-type-filters" id="netTypeFilters">
44
- <button class="net-type-btn active" data-type="all">All</button>
45
- <button class="net-type-btn" data-type="fetch">Fetch/XHR</button>
46
- <button class="net-type-btn" data-type="js">JS</button>
47
- <button class="net-type-btn" data-type="css">CSS</button>
48
- <button class="net-type-btn" data-type="img">Img</button>
49
- <button class="net-type-btn" data-type="media">Media</button>
50
- <button class="net-type-btn" data-type="font">Font</button>
51
- <button class="net-type-btn" data-type="doc">Doc</button>
52
- <button class="net-type-btn" data-type="ws">WS</button>
53
- </div>
54
- <div class="net-throttle" id="netThrottle">
55
- <select id="netThrottleSelect" class="net-throttle-select">
56
- <option value="none">No throttling</option>
57
- <option value="fast3g">Fast 3G</option>
58
- <option value="slow3g">Slow 3G</option>
59
- <option value="offline">Offline</option>
60
- </select>
61
- </div>
62
- </div>
63
- <div class="net-layout">
64
- <div class="net-table-wrap" id="netTableWrap">
65
- <div class="net-header" id="netHeader"></div>
66
- <div class="net-rows" id="netRows">
67
- <div class="empty-state" id="networkEmpty">
68
- <div class="icon">📡</div>
69
- <div class="label">No requests yet</div>
70
- <div class="hint">API calls will appear here automatically</div>
71
- </div>
72
- </div>
73
- </div>
74
- <div class="net-detail-pane" id="netDetailPane">
75
- <div class="net-detail-bar">
76
- <div class="detail-tabs" id="netDetailTabs"></div>
77
- <button class="detail-close" id="netDetailClose" title="Close">&times;</button>
78
- </div>
79
- <div class="detail-content" id="netDetailContent"></div>
80
- </div>
81
- </div>`;
82
-
83
- $('netToggle').addEventListener('change', (e) => {
84
- state.network.enabled = e.target.checked;
85
- $('netToggleText').textContent = e.target.checked ? 'Capture ON' : 'Capture OFF';
86
- window.electronAPI?.setNetworkCapture(e.target.checked);
87
- });
88
-
89
- // Network search input
90
- $('netSearchInput').addEventListener('input', (e) => {
91
- state.network.searchFilter = e.target.value.toLowerCase().trim();
92
- renderNetwork();
93
- });
94
-
95
- // Type filter buttons
96
- $('netTypeFilters').addEventListener('click', (e) => {
97
- const btn = e.target.closest('.net-type-btn');
98
- if (!btn) return;
99
- $('netTypeFilters').querySelectorAll('.net-type-btn').forEach(b => b.classList.remove('active'));
100
- btn.classList.add('active');
101
- state.network.typeFilter = btn.dataset.type;
102
- renderNetwork();
103
- });
104
-
105
- // Throttle select
106
- $('netThrottleSelect').addEventListener('change', (e) => {
107
- state.network.throttle = e.target.value;
108
- // Send throttle config to the RN app
109
- window.electronAPI?.setNetworkThrottle(state.network.throttle);
110
- });
111
-
112
- // Close detail button
113
- $('netDetailClose').addEventListener('click', closeNetDetail);
114
-
115
- buildNetHeader();
116
- }
117
-
118
- // ─── Column header with sort icons + resize ─────────────────────────────────
119
- function buildNetHeader() {
120
- const header = $('netHeader');
121
- header.innerHTML = '';
122
- NET_COLS.forEach((col, i) => {
123
- const cell = document.createElement('div');
124
- cell.className = 'net-hcell';
125
- cell.style.width = col.width + 'px';
126
- cell.dataset.col = col.key;
127
-
128
- // Label
129
- const label = document.createElement('span');
130
- label.className = 'net-hcell-label';
131
- label.textContent = col.label;
132
- cell.appendChild(label);
133
-
134
- // Sort icon
135
- if (col.key !== 'waterfall') {
136
- const sortIcon = document.createElement('span');
137
- sortIcon.className = 'net-sort-icon';
138
- if (state.network.sortCol === col.key) {
139
- sortIcon.textContent = state.network.sortDir === 'asc' ? ' \u25B2' : ' \u25BC';
140
- sortIcon.classList.add('active');
141
- }
142
- cell.appendChild(sortIcon);
143
-
144
- // Click to sort
145
- cell.addEventListener('click', (e) => {
146
- if (e.target.closest('.net-hcell-resize')) return; // don't sort on resize drag
147
- if (state.network.sortCol === col.key) {
148
- state.network.sortDir = state.network.sortDir === 'asc' ? 'desc' : 'asc';
149
- } else {
150
- state.network.sortCol = col.key;
151
- state.network.sortDir = col.key === 'name' ? 'asc' : 'desc';
152
- }
153
- buildNetHeader(); // refresh sort icons
154
- renderNetwork();
155
- });
156
- cell.style.cursor = 'pointer';
157
- }
158
-
159
- // Resize handle (not on last column)
160
- if (i < NET_COLS.length - 1) {
161
- const handle = document.createElement('div');
162
- handle.className = 'net-hcell-resize';
163
- handle.addEventListener('mousedown', (e) => startColResize(e, col, cell));
164
- cell.appendChild(handle);
165
- }
166
- header.appendChild(cell);
167
- });
168
- }
169
-
170
- function startColResize(e, col, cell) {
171
- e.preventDefault();
172
- const startX = e.clientX;
173
- const startW = col.width;
174
- const handle = e.target;
175
- handle.classList.add('active');
176
-
177
- function onMove(ev) {
178
- const delta = ev.clientX - startX;
179
- col.width = Math.max(col.min, startW + delta);
180
- cell.style.width = col.width + 'px';
181
- // Update all rows for this column
182
- document.querySelectorAll(`.net-cell[data-col="${col.key}"], .net-hcell[data-col="${col.key}"]`)
183
- .forEach(el => el.style.width = col.width + 'px');
184
- }
185
- function onUp() {
186
- handle.classList.remove('active');
187
- document.removeEventListener('mousemove', onMove);
188
- document.removeEventListener('mouseup', onUp);
189
- }
190
- document.addEventListener('mousemove', onMove);
191
- document.addEventListener('mouseup', onUp);
192
- }
193
-
194
- // ─── Network type matching ──────────────────────────────────────────────────
195
- function matchNetType(r, type) {
196
- const ct = (r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '').toLowerCase();
197
- const url = (r.url || '').toLowerCase();
198
- switch (type) {
199
- case 'fetch': return true; // All XHR/fetch requests pass
200
- case 'js': return ct.includes('javascript') || url.endsWith('.js') || url.endsWith('.bundle');
201
- case 'css': return ct.includes('css') || url.endsWith('.css');
202
- case 'img': return ct.includes('image') || /\.(png|jpg|jpeg|gif|svg|webp|ico)(\?|$)/i.test(url);
203
- case 'media': return ct.includes('video') || ct.includes('audio') || /\.(mp4|mp3|wav|webm)(\?|$)/i.test(url);
204
- case 'font': return ct.includes('font') || /\.(woff2?|ttf|otf|eot)(\?|$)/i.test(url);
205
- case 'doc': return ct.includes('html') || ct.includes('xml');
206
- case 'ws': return url.startsWith('ws://') || url.startsWith('wss://');
207
- default: return true;
208
- }
209
- }
210
-
211
- let _netRAF = null;
212
-
213
- function handleNetworkEvent(event) {
214
- if (event.type === 'console') { addConsoleLog(event); return; }
215
- if (event.type !== 'network') return;
216
- if (!state.network.enabled) return;
217
-
218
- const { id, phase } = event;
219
- if (phase === 'request') {
220
- state.network.requests[id] = { ...event, _tab: 'headers' };
221
- if (!state.network.order.includes(id)) state.network.order.push(id);
222
- $('nBadge').textContent = state.network.order.length;
223
- } else {
224
- Object.assign(state.network.requests[id] || (state.network.requests[id] = {}), event);
225
- }
226
- if (!_netRAF) {
227
- _netRAF = requestAnimationFrame(() => {
228
- _netRAF = null;
229
- renderNetwork();
230
- });
231
- }
232
- }
233
-
234
- // ─── Sort network IDs ───────────────────────────────────────────────────────
235
- function sortNetworkIds(ids) {
236
- const { sortCol, sortDir } = state.network;
237
- const reqs = state.network.requests;
238
- const sorted = [...ids].sort((a, b) => {
239
- const ra = reqs[a], rb = reqs[b];
240
- if (!ra || !rb) return 0;
241
- let va, vb;
242
- switch (sortCol) {
243
- case 'name':
244
- va = (ra.url || '').toLowerCase(); vb = (rb.url || '').toLowerCase();
245
- return va < vb ? -1 : va > vb ? 1 : 0;
246
- case 'status':
247
- va = ra.status || 0; vb = rb.status || 0;
248
- return va - vb;
249
- case 'type':
250
- va = (ra.responseHeaders?.['content-type'] || '').toLowerCase();
251
- vb = (rb.responseHeaders?.['content-type'] || '').toLowerCase();
252
- return va < vb ? -1 : va > vb ? 1 : 0;
253
- case 'size':
254
- va = typeof ra.responseBody === 'string' ? ra.responseBody.length : (ra.responseBody ? JSON.stringify(ra.responseBody).length : 0);
255
- vb = typeof rb.responseBody === 'string' ? rb.responseBody.length : (rb.responseBody ? JSON.stringify(rb.responseBody).length : 0);
256
- return va - vb;
257
- case 'time':
258
- default:
259
- va = ra.ts || 0; vb = rb.ts || 0;
260
- return va - vb;
261
- }
262
- });
263
- if (sortDir === 'desc') sorted.reverse();
264
- return sorted;
265
- }
266
-
267
- // ─── Render network rows ────────────────────────────────────────────────────
268
- function renderNetwork() {
269
- const rows = $('netRows');
270
- const empty = $('networkEmpty');
271
- if (!rows) return;
272
-
273
- const { statusFilter, typeFilter, searchFilter } = state.network;
274
- const visible = state.network.order.filter(id => {
275
- const r = state.network.requests[id];
276
- if (!r) return false;
277
- if (statusFilter === '2xx' && !(r.status >= 200 && r.status < 300)) return false;
278
- if (statusFilter === 'errors' && !(r.phase === 'error' || r.status >= 400)) return false;
279
- // Global filter OR network-specific search
280
- const filterTerm = searchFilter || state.filter;
281
- if (filterTerm && !r.url?.toLowerCase().includes(filterTerm)) return false;
282
- if (typeFilter !== 'all' && !matchNetType(r, typeFilter)) return false;
283
- return true;
284
- });
285
-
286
- // Sort: apply current sort, default = newest first
287
- const sortedVisible = sortNetworkIds(visible);
288
-
289
- empty.style.display = sortedVisible.length ? 'none' : 'flex';
290
- rows.querySelectorAll('.net-row').forEach(e => e.remove());
291
-
292
- // Waterfall scale: find min/max timestamps
293
- let wfMin = Infinity, wfMax = 0;
294
- sortedVisible.forEach(id => {
295
- const r = state.network.requests[id];
296
- if (r.ts) { wfMin = Math.min(wfMin, r.ts); wfMax = Math.max(wfMax, r.ts + (r.duration || 0)); }
297
- });
298
- const wfRange = Math.max(wfMax - wfMin, 1);
299
-
300
- const frag = document.createDocumentFragment();
301
- sortedVisible.forEach(id => {
302
- const r = state.network.requests[id];
303
- frag.appendChild(buildNetRow(r, wfMin, wfRange));
304
- });
305
- rows.appendChild(frag);
306
- }
307
-
308
- function buildNetRow(r, wfMin, wfRange) {
309
- const row = document.createElement('div');
310
- row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (r.phase === 'error' ? ' error' : '');
311
- row.dataset.id = r.id;
312
-
313
- const urlObj = tryURL(r.url);
314
- const pathname = urlObj ? urlObj.pathname : r.url || '';
315
- const filename = pathname.split('/').filter(Boolean).pop() || pathname;
316
- const host = urlObj ? urlObj.host : '';
317
-
318
- // Name — show method + full path (expands with column)
319
- const nameCell = document.createElement('div');
320
- nameCell.className = 'net-cell net-cell-name';
321
- nameCell.dataset.col = 'name';
322
- nameCell.style.width = NET_COLS[0].width + 'px';
323
- const method = r.method || '?';
324
- const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
325
- const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
326
- nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
327
- row.appendChild(nameCell);
328
-
329
- // Status
330
- const statusCell = document.createElement('div');
331
- statusCell.className = 'net-cell net-status';
332
- statusCell.dataset.col = 'status';
333
- statusCell.style.width = NET_COLS[1].width + 'px';
334
- let statusStr = '...', sCls = 's-pending';
335
- if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
336
- else if (r.status) { statusStr = String(r.status); sCls = `s-${Math.floor(r.status/100)}`; }
337
- statusCell.className += ` ${sCls}`;
338
- statusCell.textContent = statusStr;
339
- row.appendChild(statusCell);
340
-
341
- // Type (content-type from response headers)
342
- const typeCell = document.createElement('div');
343
- typeCell.className = 'net-cell net-type';
344
- typeCell.dataset.col = 'type';
345
- typeCell.style.width = NET_COLS[2].width + 'px';
346
- const ct = r.responseHeaders?.['content-type'] || r.responseHeaders?.['Content-Type'] || '';
347
- typeCell.textContent = ct.split(';')[0].replace('application/', '').replace('text/', '') || '—';
348
- row.appendChild(typeCell);
349
-
350
- // Initiator
351
- const initCell = document.createElement('div');
352
- initCell.className = 'net-cell net-initiator';
353
- initCell.dataset.col = 'initiator';
354
- initCell.style.width = NET_COLS[3].width + 'px';
355
- initCell.textContent = r.initiator || 'xhr';
356
- row.appendChild(initCell);
357
-
358
- // Size
359
- const sizeCell = document.createElement('div');
360
- sizeCell.className = 'net-cell net-size';
361
- sizeCell.dataset.col = 'size';
362
- sizeCell.style.width = NET_COLS[4].width + 'px';
363
- const bodyStr = typeof r.responseBody === 'string' ? r.responseBody : (r.responseBody != null ? JSON.stringify(r.responseBody) : '');
364
- sizeCell.textContent = bodyStr.length > 0 ? formatSize(bodyStr.length) : '—';
365
- row.appendChild(sizeCell);
366
-
367
- // Time
368
- const timeCell = document.createElement('div');
369
- timeCell.className = 'net-cell net-time' + ((r.duration || 0) > 1500 ? ' slow' : '');
370
- timeCell.dataset.col = 'time';
371
- timeCell.style.width = NET_COLS[5].width + 'px';
372
- timeCell.textContent = r.duration != null ? (r.duration > 999 ? `${(r.duration/1000).toFixed(1)}s` : `${r.duration}ms`) : '...';
373
- row.appendChild(timeCell);
374
-
375
- // Waterfall
376
- const wfCell = document.createElement('div');
377
- wfCell.className = 'net-cell net-waterfall';
378
- wfCell.dataset.col = 'waterfall';
379
- wfCell.style.width = NET_COLS[6].width + 'px';
380
- if (r.ts) {
381
- const left = ((r.ts - wfMin) / wfRange) * 100;
382
- const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
383
- let barCls = 'pending';
384
- if (r.phase === 'error') barCls = 'err';
385
- else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
386
- wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
387
- }
388
- row.appendChild(wfCell);
389
-
390
- // Click to select and show detail
391
- row.addEventListener('click', () => selectNetRequest(r.id));
392
-
393
- // Right-click for context menu (copy as cURL)
394
- row.addEventListener('contextmenu', (e) => {
395
- e.preventDefault();
396
- showNetContextMenu(e, r);
397
- });
398
-
399
- return row;
400
- }
401
-
402
- // ─── Select request → show detail pane below the table (same layout) ────────
403
- function selectNetRequest(id) {
404
- state.network.selectedId = id;
405
- const r = state.network.requests[id];
406
- if (!r) return;
407
-
408
- // Highlight selected row
409
- document.querySelectorAll('#netRows .net-row').forEach(el =>
410
- el.classList.toggle('selected', el.dataset.id === id)
411
- );
412
-
413
- // Show detail pane below
414
- const pane = $('netDetailPane');
415
- pane.classList.add('open');
416
- r._tab = r._tab || 'headers';
417
- renderNetDetailTabs(r);
418
- renderNetDetailContent(r);
419
- }
420
-
421
- function closeNetDetail() {
422
- state.network.selectedId = null;
423
- const pane = $('netDetailPane');
424
- if (pane) pane.classList.remove('open');
425
- document.querySelectorAll('#netRows .net-row').forEach(el =>
426
- el.classList.remove('selected')
427
- );
428
- }
429
-
430
- function renderNetDetailTabs(r) {
431
- const tabs = $('netDetailTabs');
432
- tabs.innerHTML = '';
433
- ['Headers', 'Request', 'Preview', 'Response'].forEach(label => {
434
- const key = label.toLowerCase();
435
- const btn = document.createElement('button');
436
- btn.className = 'detail-tab' + (r._tab === key ? ' active' : '');
437
- btn.textContent = label;
438
- btn.addEventListener('click', () => {
439
- r._tab = key;
440
- tabs.querySelectorAll('.detail-tab').forEach(b => b.classList.remove('active'));
441
- btn.classList.add('active');
442
- renderNetDetailContent(r);
443
- });
444
- tabs.appendChild(btn);
445
- });
446
- }
447
-
448
- function renderNetDetailContent(r) {
449
- const body = $('netDetailContent');
450
- if (!body) return;
451
- const tab = r._tab || 'headers';
452
-
453
- if (tab === 'headers') {
454
- const rqH = r.requestHeaders || {};
455
- const rsH = r.responseHeaders || {};
456
- const renderH = (title, h) => {
457
- const keys = Object.keys(h);
458
- if (!keys.length) return `<div class="section-label">${title}</div><span style="color:var(--text-dim)">none</span>`;
459
- return `<div class="section-label">${title}</div><div class="kv-grid">${keys.map(k => `<span class="kv-key">${esc(k)}</span><span class="kv-val">${esc(h[k])}</span>`).join('')}</div>`;
460
- };
461
- body.innerHTML = `<div class="section-label" style="margin-top:0">General</div>
462
- <div class="kv-grid">
463
- <span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
464
- <span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
465
- <span class="kv-key">Status</span><span class="kv-val ${r.status ? 's-' + Math.floor(r.status/100) : 's-pending'}">${r.status || 'Pending'} ${r.statusText || ''}</span>
466
- </div>
467
- ${renderH('Response Headers', rsH)}
468
- ${renderH('Request Headers', rqH)}`;
469
- } else if (tab === 'request') {
470
- body.innerHTML = r.requestBody ? renderJSON(r.requestBody) : '<span style="color:var(--text-dim)">No request body</span>';
471
- } else if (tab === 'preview') {
472
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
473
- if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
474
- // Render as collapsible JSON tree with right-click copy
475
- const val = r.responseBody;
476
- let treeData = val;
477
- if (typeof val === 'string') {
478
- try { treeData = JSON.parse(val); } catch { body.textContent = val; return; }
479
- }
480
- if (treeData && typeof treeData === 'object') {
481
- body.innerHTML = '';
482
- body.appendChild(createTreeNode(null, treeData, false));
483
- // Right-click on preview to copy the whole object or clicked node value
484
- body.addEventListener('contextmenu', (e) => {
485
- e.preventDefault();
486
- showPreviewCopyMenu(e, treeData);
487
- });
488
- } else {
489
- body.innerHTML = '<span style="color:var(--text-dim)">No preview available</span>';
490
- }
491
- } else if (tab === 'response') {
492
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
493
- if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
494
- body.innerHTML = renderJSON(r.responseBody);
495
- }
496
- }
497
-
498
- // ─── Network context menus ──────────────────────────────────────────────────
499
- function showNetContextMenu(e, r) {
500
- const items = [
501
- { label: 'Copy as cURL', action: () => navigator.clipboard.writeText(buildCurlCommand(r)) },
502
- { label: 'Copy URL', action: () => navigator.clipboard.writeText(r.url || '') },
503
- ];
504
- if (r.responseBody) {
505
- items.push({ label: 'Copy Response', action: () => {
506
- const text = typeof r.responseBody === 'string' ? r.responseBody : JSON.stringify(r.responseBody, null, 2);
507
- navigator.clipboard.writeText(text);
508
- }});
509
- }
510
- showContextMenu(e, items);
511
- }
512
-
513
- function showPreviewCopyMenu(e, fullData) {
514
- const items = [
515
- { label: 'Copy Object', action: () => navigator.clipboard.writeText(JSON.stringify(fullData, null, 2)) },
516
- ];
517
- const sel = window.getSelection();
518
- if (sel && sel.toString().length > 0) {
519
- items.push({ label: 'Copy Selection', action: () => navigator.clipboard.writeText(sel.toString()) });
520
- }
521
- const keyEl = e.target.closest('.ov-key');
522
- const leafEl = e.target.closest('.ov-leaf');
523
- if (keyEl || leafEl) {
524
- items.push({ label: 'Copy Value', action: () => navigator.clipboard.writeText((leafEl || keyEl.parentElement).textContent) });
525
- }
526
- showContextMenu(e, items);
527
- }
528
-
529
- function buildCurlCommand(r) {
530
- let cmd = `curl '${r.url}'`;
531
- if (r.method && r.method !== 'GET') cmd += ` -X ${r.method}`;
532
- const headers = r.requestHeaders || {};
533
- Object.entries(headers).forEach(([k, v]) => {
534
- cmd += ` \\\n -H '${k}: ${v}'`;
535
- });
536
- if (r.requestBody) {
537
- const body = typeof r.requestBody === 'string' ? r.requestBody : JSON.stringify(r.requestBody);
538
- cmd += ` \\\n --data-raw '${body.replace(/'/g, "'\\''")}'`;
539
- }
540
- return cmd;
541
- }
542
-
543
- // ─── Export via window.RNDebug ────────────────────────────────────────────────
544
- window.RNDebug.NET_COLS = NET_COLS;
545
- window.RNDebug.initNetworkPanel = initNetworkPanel;
546
- window.RNDebug.buildNetHeader = buildNetHeader;
547
- window.RNDebug.startColResize = startColResize;
548
- window.RNDebug.matchNetType = matchNetType;
549
- window.RNDebug.sortNetworkIds = sortNetworkIds;
550
- window.RNDebug.handleNetworkEvent = handleNetworkEvent;
551
- window.RNDebug.renderNetwork = renderNetwork;
552
- window.RNDebug.buildNetRow = buildNetRow;
553
- window.RNDebug.selectNetRequest = selectNetRequest;
554
- window.RNDebug.closeNetDetail = closeNetDetail;
555
- window.RNDebug.renderNetDetailTabs = renderNetDetailTabs;
556
- window.RNDebug.renderNetDetailContent = renderNetDetailContent;
557
- window.RNDebug.showNetContextMenu = showNetContextMenu;
558
- window.RNDebug.showPreviewCopyMenu = showPreviewCopyMenu;
559
- window.RNDebug.buildCurlCommand = buildCurlCommand;