reactoradar 1.6.2 → 1.6.4

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.
package/README.md CHANGED
@@ -27,14 +27,14 @@
27
27
 
28
28
  ### Console — Interactive Log Viewer
29
29
  <p align="center">
30
- <img src="https://raw.githubusercontent.com/sharanagouda/react-native-debugger/main/screenshots/consoleLogs.png" alt="Console Panel" width="800" />
30
+ <img src="https://raw.githubusercontent.com/sharanagouda/reactoradar/main/screenshots/consoleLogs.png" alt="Console Panel" width="800" />
31
31
  </p>
32
32
 
33
33
  *Collapsible object trees, multi-select level filters, log grouping, search, export as JSON, right-click to copy*
34
34
 
35
35
  ### Network — Chrome DevTools-style Inspector
36
36
  <p align="center">
37
- <img src="https://raw.githubusercontent.com/sharanagouda/react-native-debugger/main/screenshots/networkLogs.png" alt="Network Panel" width="800" />
37
+ <img src="https://raw.githubusercontent.com/sharanagouda/reactoradar/main/screenshots/networkLogs.png" alt="Network Panel" width="800" />
38
38
  </p>
39
39
 
40
40
  *Resizable/sortable columns, slow API highlights, export as HAR, stats bar, hide unwanted URLs, throttling*
@@ -85,7 +85,7 @@ npx reactoradar # Launch the debugger
85
85
 
86
86
  ### Option B: Download .dmg
87
87
 
88
- 1. Download from [Releases](https://github.com/sharanagouda/react-native-debugger/releases)
88
+ 1. Download from [Releases](https://github.com/sharanagouda/reactoradar/releases)
89
89
  2. Drag **ReactoRadar** to Applications
90
90
  3. Install the SDK: `npx reactoradar setup` from your RN project
91
91
  4. Open ReactoRadar from Applications
@@ -101,8 +101,8 @@ npm install -g reactoradar
101
101
  ### Option D: Build from source
102
102
 
103
103
  ```bash
104
- git clone https://github.com/sharanagouda/react-native-debugger.git
105
- cd react-native-debugger
104
+ git clone https://github.com/sharanagouda/reactoradar.git
105
+ cd reactoradar
106
106
  npm install
107
107
  npm start # dev mode
108
108
  npm run build # build .dmg
@@ -314,8 +314,8 @@ If ReactoRadar helps your workflow, consider supporting development:
314
314
  Contributions welcome! Fork → branch → PR.
315
315
 
316
316
  ```bash
317
- git clone https://github.com/sharanagouda/react-native-debugger.git
318
- cd react-native-debugger
317
+ git clone https://github.com/sharanagouda/reactoradar.git
318
+ cd reactoradar
319
319
  npm install
320
320
  npm start
321
321
  ```
package/app.js CHANGED
@@ -303,6 +303,17 @@ if (window.electronAPI) {
303
303
 
304
304
  // Cmd+F — focus the search input for the active panel
305
305
  function _handleFind() {
306
+ // If network detail is open, focus the detail search
307
+ if (state.activePanel === 'network' && state.network.selectedId) {
308
+ const wrap = $('detailSearchWrap');
309
+ const input = $('detailSearchInput');
310
+ if (wrap && input) {
311
+ wrap.style.display = 'flex';
312
+ input.focus();
313
+ input.select();
314
+ return;
315
+ }
316
+ }
306
317
  const searchMap = {
307
318
  console: 'consoleSearch',
308
319
  network: 'netSearchInput',
@@ -366,6 +377,7 @@ function _applyUpdateBanner() {
366
377
  if (!info) return;
367
378
  const { current, latest, autoUpdate } = info;
368
379
  const downloaded = state._updateDownloaded;
380
+ const targetVersion = downloaded || latest;
369
381
 
370
382
  const el = $('aboutVersion');
371
383
  if (el) {
@@ -376,39 +388,86 @@ function _applyUpdateBanner() {
376
388
  }
377
389
  }
378
390
 
379
- // Remove old button if state changed
391
+ // Remove old buttons if state changed
380
392
  const oldBtn = $('updateBtn');
381
393
  if (oldBtn && downloaded && !oldBtn.dataset.isRestart) oldBtn.parentElement?.remove();
394
+ const oldChangelog = $('changelogBtn');
395
+ if (oldChangelog && downloaded && !oldChangelog.dataset.updated) oldChangelog.remove();
396
+
397
+ const aboutEl = document.querySelector('.settings-about');
398
+ if (!aboutEl) return;
399
+
400
+ // Add "What's new?" link
401
+ if (!$('changelogBtn')) {
402
+ const link = document.createElement('div');
403
+ link.style.cssText = 'margin-top:6px;text-align:center';
404
+ link.innerHTML = `<span id="changelogBtn" class="about-link" style="font-size:10px;cursor:pointer" data-updated="${downloaded ? '1' : ''}">What's new in v${targetVersion}?</span>`;
405
+ aboutEl.appendChild(link);
406
+ $('changelogBtn')?.addEventListener('click', () => _showChangelog(targetVersion));
407
+ }
382
408
 
383
- // Add update button in settings if not already there
409
+ // Add update button
384
410
  if (!$('updateBtn')) {
385
- const aboutEl = document.querySelector('.settings-about');
386
- if (aboutEl) {
387
- const btn = document.createElement('div');
388
- btn.style.cssText = 'margin-top:10px';
389
- if (downloaded) {
390
- // Update is downloaded — show "Restart & Update"
391
- btn.innerHTML = '<button id="updateBtn" data-is-restart="1" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Restart & Update to v' + downloaded + '</button>';
392
- aboutEl.appendChild(btn);
393
- $('updateBtn')?.addEventListener('click', () => {
394
- window.electronAPI?.installUpdate();
395
- });
396
- } else if (autoUpdate) {
397
- // Auto-update in progress — show downloading status
398
- btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
399
- aboutEl.appendChild(btn);
400
- } else {
401
- // npx/manual — show download link
402
- btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
403
- aboutEl.appendChild(btn);
404
- $('updateBtn')?.addEventListener('click', () => {
405
- window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
406
- });
407
- }
411
+ const btn = document.createElement('div');
412
+ btn.style.cssText = 'margin-top:8px;text-align:center';
413
+ if (downloaded) {
414
+ btn.innerHTML = '<button id="updateBtn" data-is-restart="1" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Restart & Update to v' + downloaded + '</button>';
415
+ aboutEl.appendChild(btn);
416
+ $('updateBtn')?.addEventListener('click', () => window.electronAPI?.installUpdate());
417
+ } else if (autoUpdate) {
418
+ btn.innerHTML = '<button id="updateBtn" class="tb-btn" style="font-size:11px;padding:6px 16px;opacity:0.7" disabled>Downloading v' + latest + '...</button>';
419
+ aboutEl.appendChild(btn);
420
+ } else {
421
+ btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
422
+ aboutEl.appendChild(btn);
423
+ $('updateBtn')?.addEventListener('click', () => window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar/releases'));
408
424
  }
409
425
  }
410
426
  }
411
427
 
428
+ async function _showChangelog(version) {
429
+ // Remove existing modal
430
+ $('changelogModal')?.remove();
431
+
432
+ const modal = document.createElement('div');
433
+ modal.id = 'changelogModal';
434
+ modal.className = 'changelog-modal-overlay';
435
+ modal.innerHTML = `
436
+ <div class="changelog-modal">
437
+ <div class="changelog-header">
438
+ <span class="changelog-title">What's New in v${esc(version)}</span>
439
+ <button class="changelog-close" id="changelogClose">&times;</button>
440
+ </div>
441
+ <div class="changelog-body" id="changelogBody">
442
+ <div style="color:var(--text-dim);padding:20px;text-align:center">Loading release notes...</div>
443
+ </div>
444
+ </div>`;
445
+ document.body.appendChild(modal);
446
+
447
+ // Close handlers
448
+ $('changelogClose')?.addEventListener('click', () => modal.remove());
449
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
450
+
451
+ // Fetch changelog
452
+ try {
453
+ const notes = await window.electronAPI?.fetchChangelog(version);
454
+ const body = $('changelogBody');
455
+ if (body && notes) {
456
+ // Simple markdown-like rendering
457
+ body.innerHTML = notes
458
+ .replace(/^### (.+)$/gm, '<h3 style="color:var(--accent);font-size:12px;font-weight:700;margin:12px 0 6px">$1</h3>')
459
+ .replace(/^## (.+)$/gm, '<h2 style="color:var(--text);font-size:14px;font-weight:700;margin:16px 0 8px">$1</h2>')
460
+ .replace(/^- \*\*(.+?)\*\*(.*)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6"><b style="color:var(--text)">$1</b><span style="color:var(--text-dim)">$2</span></div>')
461
+ .replace(/^- (.+)$/gm, '<div style="margin:3px 0;font-size:11px;line-height:1.6;color:var(--text-mid)">• $1</div>')
462
+ .replace(/`([^`]+)`/g, '<code style="background:var(--bg3);padding:1px 4px;border-radius:3px;color:var(--accent);font-size:10px">$1</code>')
463
+ .replace(/\n\n/g, '<br/>');
464
+ }
465
+ } catch {
466
+ const body = $('changelogBody');
467
+ if (body) body.innerHTML = '<div style="color:var(--red);padding:20px;text-align:center">Could not fetch release notes</div>';
468
+ }
469
+ }
470
+
412
471
  function updateDeviceBanner(service, connected) {
413
472
  state.connections[service] = connected;
414
473
  const el = $('deviceStatus');
@@ -1255,6 +1314,13 @@ function initNetworkPanel() {
1255
1314
  <div class="net-detail-pane" id="netDetailPane">
1256
1315
  <div class="net-detail-bar">
1257
1316
  <div class="detail-tabs" id="netDetailTabs"></div>
1317
+ <div class="detail-search-wrap" id="detailSearchWrap" style="display:none">
1318
+ <input id="detailSearchInput" class="detail-search-input" placeholder="Search key or value..." />
1319
+ <span id="detailSearchCount" class="detail-search-count"></span>
1320
+ <button class="detail-search-nav" id="detailSearchPrev" title="Previous">&#9650;</button>
1321
+ <button class="detail-search-nav" id="detailSearchNext" title="Next">&#9660;</button>
1322
+ <button class="detail-search-close" id="detailSearchClose" title="Close search">&times;</button>
1323
+ </div>
1258
1324
  <button class="detail-close" id="netDetailClose" title="Close">&times;</button>
1259
1325
  </div>
1260
1326
  <div class="detail-content" id="netDetailContent"></div>
@@ -1408,6 +1474,121 @@ function initNetworkPanel() {
1408
1474
  // Close detail button
1409
1475
  $('netDetailClose').addEventListener('click', closeNetDetail);
1410
1476
 
1477
+ // Detail panel search
1478
+ let _detailSearchMatches = [];
1479
+ let _detailSearchIdx = -1;
1480
+
1481
+ function _detailSearch() {
1482
+ const term = $('detailSearchInput')?.value?.trim().toLowerCase();
1483
+ const body = $('netDetailContent');
1484
+ if (!body || !term) { _detailClearSearch(); return; }
1485
+
1486
+ // Remove old highlights
1487
+ body.querySelectorAll('.detail-search-hl').forEach(el => {
1488
+ const parent = el.parentNode;
1489
+ parent.replaceChild(document.createTextNode(el.textContent), el);
1490
+ parent.normalize();
1491
+ });
1492
+
1493
+ _detailSearchMatches = [];
1494
+ _detailSearchIdx = -1;
1495
+
1496
+ // Walk all text nodes and highlight matches
1497
+ const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
1498
+ const textNodes = [];
1499
+ while (walker.nextNode()) textNodes.push(walker.currentNode);
1500
+
1501
+ textNodes.forEach(node => {
1502
+ const text = node.textContent;
1503
+ const lower = text.toLowerCase();
1504
+ if (!lower.includes(term)) return;
1505
+
1506
+ const frag = document.createDocumentFragment();
1507
+ let lastIdx = 0;
1508
+ let idx;
1509
+ while ((idx = lower.indexOf(term, lastIdx)) !== -1) {
1510
+ if (idx > lastIdx) frag.appendChild(document.createTextNode(text.slice(lastIdx, idx)));
1511
+ const hl = document.createElement('span');
1512
+ hl.className = 'detail-search-hl';
1513
+ hl.textContent = text.slice(idx, idx + term.length);
1514
+ _detailSearchMatches.push(hl);
1515
+ frag.appendChild(hl);
1516
+ lastIdx = idx + term.length;
1517
+ }
1518
+ if (lastIdx < text.length) frag.appendChild(document.createTextNode(text.slice(lastIdx)));
1519
+ node.parentNode.replaceChild(frag, node);
1520
+ });
1521
+
1522
+ // Update count
1523
+ const countEl = $('detailSearchCount');
1524
+ if (countEl) countEl.textContent = _detailSearchMatches.length ? `${_detailSearchMatches.length} found` : 'No match';
1525
+
1526
+ // Navigate to first match
1527
+ if (_detailSearchMatches.length) _detailNavTo(0);
1528
+ }
1529
+
1530
+ function _detailNavTo(idx) {
1531
+ // Remove active highlight from previous
1532
+ if (_detailSearchIdx >= 0 && _detailSearchMatches[_detailSearchIdx]) {
1533
+ _detailSearchMatches[_detailSearchIdx].classList.remove('active');
1534
+ }
1535
+ _detailSearchIdx = idx;
1536
+ const el = _detailSearchMatches[idx];
1537
+ if (!el) return;
1538
+ el.classList.add('active');
1539
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
1540
+ // Update count
1541
+ const countEl = $('detailSearchCount');
1542
+ if (countEl) countEl.textContent = `${idx + 1}/${_detailSearchMatches.length}`;
1543
+ }
1544
+
1545
+ function _detailClearSearch() {
1546
+ const body = $('netDetailContent');
1547
+ if (body) {
1548
+ body.querySelectorAll('.detail-search-hl').forEach(el => {
1549
+ const parent = el.parentNode;
1550
+ parent.replaceChild(document.createTextNode(el.textContent), el);
1551
+ parent.normalize();
1552
+ });
1553
+ }
1554
+ _detailSearchMatches = [];
1555
+ _detailSearchIdx = -1;
1556
+ const countEl = $('detailSearchCount');
1557
+ if (countEl) countEl.textContent = '';
1558
+ }
1559
+
1560
+ $('detailSearchInput')?.addEventListener('input', () => {
1561
+ clearTimeout($('detailSearchInput')._debounce);
1562
+ $('detailSearchInput')._debounce = setTimeout(_detailSearch, 200);
1563
+ });
1564
+ $('detailSearchInput')?.addEventListener('keydown', (e) => {
1565
+ if (e.key === 'Enter') {
1566
+ e.preventDefault();
1567
+ if (!_detailSearchMatches.length) return;
1568
+ const next = e.shiftKey
1569
+ ? (_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length
1570
+ : (_detailSearchIdx + 1) % _detailSearchMatches.length;
1571
+ _detailNavTo(next);
1572
+ }
1573
+ if (e.key === 'Escape') {
1574
+ _detailClearSearch();
1575
+ $('detailSearchWrap').style.display = 'none';
1576
+ }
1577
+ });
1578
+ $('detailSearchNext')?.addEventListener('click', () => {
1579
+ if (!_detailSearchMatches.length) return;
1580
+ _detailNavTo((_detailSearchIdx + 1) % _detailSearchMatches.length);
1581
+ });
1582
+ $('detailSearchPrev')?.addEventListener('click', () => {
1583
+ if (!_detailSearchMatches.length) return;
1584
+ _detailNavTo((_detailSearchIdx - 1 + _detailSearchMatches.length) % _detailSearchMatches.length);
1585
+ });
1586
+ $('detailSearchClose')?.addEventListener('click', () => {
1587
+ _detailClearSearch();
1588
+ $('detailSearchInput').value = '';
1589
+ $('detailSearchWrap').style.display = 'none';
1590
+ });
1591
+
1411
1592
  buildNetHeader();
1412
1593
  }
1413
1594
 
@@ -1828,14 +2009,38 @@ function closeNetDetail() {
1828
2009
  );
1829
2010
  }
1830
2011
 
2012
+ function _estimateSize(val) {
2013
+ if (val == null) return 0;
2014
+ if (typeof val === 'string') return val.length;
2015
+ try { return JSON.stringify(val).length; } catch { return 0; }
2016
+ }
2017
+
2018
+ function _formatBytes(bytes) {
2019
+ if (bytes < 1024) return `${bytes}B`;
2020
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
2021
+ return `${(bytes / 1048576).toFixed(1)}MB`;
2022
+ }
2023
+
1831
2024
  function renderNetDetailTabs(r) {
1832
2025
  const tabs = $('netDetailTabs');
1833
2026
  tabs.innerHTML = '';
1834
- ['Headers', 'Request', 'Preview', 'Response'].forEach(label => {
1835
- const key = label.toLowerCase();
2027
+
2028
+ const tabDefs = [
2029
+ { label: 'Headers', key: 'headers' },
2030
+ { label: 'Request', key: 'request', sizeFrom: 'requestBody' },
2031
+ { label: 'Preview', key: 'preview', sizeFrom: 'responseBody' },
2032
+ { label: 'Response', key: 'response', sizeFrom: 'responseBody' },
2033
+ ];
2034
+
2035
+ tabDefs.forEach(({ label, key, sizeFrom }) => {
1836
2036
  const btn = document.createElement('button');
1837
2037
  btn.className = 'detail-tab' + (r._tab === key ? ' active' : '');
1838
- btn.textContent = label;
2038
+ let text = label;
2039
+ if (sizeFrom && r[sizeFrom]) {
2040
+ const size = _estimateSize(r[sizeFrom]);
2041
+ if (size > 0) text += ` (${_formatBytes(size)})`;
2042
+ }
2043
+ btn.textContent = text;
1839
2044
  btn.addEventListener('click', () => {
1840
2045
  r._tab = key;
1841
2046
  tabs.querySelectorAll('.detail-tab').forEach(b => b.classList.remove('active'));
@@ -1844,6 +2049,12 @@ function renderNetDetailTabs(r) {
1844
2049
  });
1845
2050
  tabs.appendChild(btn);
1846
2051
  });
2052
+
2053
+ // Show search box for Preview/Response tabs
2054
+ const searchWrap = $('detailSearchWrap');
2055
+ if (searchWrap) {
2056
+ searchWrap.style.display = (r._tab === 'preview' || r._tab === 'response' || r._tab === 'headers') ? 'flex' : 'none';
2057
+ }
1847
2058
  }
1848
2059
 
1849
2060
  function renderNetDetailContent(r) {
@@ -3694,10 +3905,10 @@ function initSettingsPanel() {
3694
3905
 
3695
3906
  // About links
3696
3907
  $('linkGithub')?.addEventListener('click', () => {
3697
- window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger');
3908
+ window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar');
3698
3909
  });
3699
3910
  $('linkDocs')?.addEventListener('click', () => {
3700
- window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger#readme');
3911
+ window.electronAPI?.openExternal('https://github.com/sharanagouda/reactoradar#readme');
3701
3912
  });
3702
3913
  $('linkLinkedIn')?.addEventListener('click', () => {
3703
3914
  window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
package/bin/setup.js CHANGED
@@ -465,7 +465,7 @@ ${SDK_MARKER_END}
465
465
  console.log(' 1. Start the debugger: ' + C.cyan + 'open "/Applications/ReactoRadar.app"' + C.reset);
466
466
  } else {
467
467
  console.log(' 1. Start the debugger: ' + C.cyan + 'npx reactoradar' + C.reset);
468
- console.log(' ' + C.dim + '(or download .dmg from https://github.com/sharanagouda/react-native-debugger/releases)' + C.reset);
468
+ console.log(' ' + C.dim + '(or download .dmg from https://github.com/sharanagouda/reactoradar/releases)' + C.reset);
469
469
  }
470
470
  console.log(' 2. Run your RN app: ' + C.cyan + 'npx react-native start --reset-cache' + C.reset);
471
471
  console.log(' 3. Console, Network, Storage auto-connect');
package/main.js CHANGED
@@ -588,6 +588,21 @@ function setupIPC() {
588
588
  }
589
589
  });
590
590
 
591
+ ipcMain.handle('fetch-changelog', async (_, version) => {
592
+ return new Promise((resolve) => {
593
+ https.get(`https://api.github.com/repos/sharanagouda/reactoradar/releases/tags/v${version}`, {
594
+ headers: { 'User-Agent': 'ReactoRadar', 'Accept': 'application/vnd.github.v3+json' }
595
+ }, (res) => {
596
+ let data = '';
597
+ res.on('data', d => data += d);
598
+ res.on('end', () => {
599
+ try { resolve(JSON.parse(data).body || 'No release notes available.'); }
600
+ catch { resolve('Could not fetch release notes.'); }
601
+ });
602
+ }).on('error', () => resolve('Could not connect to GitHub.'));
603
+ });
604
+ });
605
+
591
606
  ipcMain.on('install-update', () => {
592
607
  if (autoUpdater) {
593
608
  autoUpdater.quitAndInstall(false, true);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.2",
4
+ "version": "1.6.4",
5
5
  "description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
6
6
  "main": "main.js",
7
7
  "bin": {
@@ -18,16 +18,16 @@
18
18
  "network-inspector",
19
19
  "hermes",
20
20
  "flipper-alternative",
21
- "react-native-debugger",
21
+ "reactoradar",
22
22
  "macos"
23
23
  ],
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "git+https://github.com/sharanagouda/react-native-debugger.git"
26
+ "url": "git+https://github.com/sharanagouda/reactoradar.git"
27
27
  },
28
- "homepage": "https://github.com/sharanagouda/react-native-debugger#readme",
28
+ "homepage": "https://github.com/sharanagouda/reactoradar#readme",
29
29
  "bugs": {
30
- "url": "https://github.com/sharanagouda/react-native-debugger/issues"
30
+ "url": "https://github.com/sharanagouda/reactoradar/issues"
31
31
  },
32
32
  "license": "MIT",
33
33
  "author": "sharanagouda",
@@ -66,7 +66,7 @@
66
66
  {
67
67
  "provider": "github",
68
68
  "owner": "sharanagouda",
69
- "repo": "react-native-debugger"
69
+ "repo": "reactoradar"
70
70
  }
71
71
  ],
72
72
  "mac": {
package/preload.js CHANGED
@@ -35,4 +35,5 @@ contextBridge.exposeInMainWorld('electronAPI', {
35
35
  startNativeLogs: (platform) => ipcRenderer.send('start-native-logs', platform),
36
36
  stopNativeLogs: () => ipcRenderer.send('stop-native-logs'),
37
37
  detectNativePlatform: () => ipcRenderer.invoke('detect-native-platform'),
38
+ fetchChangelog: (version) => ipcRenderer.invoke('fetch-changelog', version),
38
39
  });
package/styles.css CHANGED
@@ -1682,6 +1682,27 @@ mark { background: rgba(79,172,255,.2); color: var(--accent); border-radius: 2px
1682
1682
  .native-fatal { background: rgba(255,94,114,.08); }
1683
1683
  .native-fatal .native-log-msg { color: var(--red); font-weight: 700; }
1684
1684
 
1685
+ /* ── Detail Panel Search ───────────────────────────────────────────────────── */
1686
+ .detail-search-wrap { display: flex; align-items: center; gap: 4px; margin-left: auto; padding: 0 6px; }
1687
+ .detail-search-input { width: 150px; font-size: 10px; padding: 3px 6px; border: 1px solid var(--border); background: var(--bg1); color: var(--text); border-radius: 3px; outline: none; }
1688
+ .detail-search-input:focus { border-color: var(--accent); }
1689
+ .detail-search-count { font-size: 9px; color: var(--text-dim); white-space: nowrap; min-width: 45px; }
1690
+ .detail-search-nav { background: transparent; border: 1px solid var(--border); color: var(--text-dim); font-size: 9px; width: 18px; height: 18px; border-radius: 3px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
1691
+ .detail-search-nav:hover { background: var(--bg3); color: var(--text); }
1692
+ .detail-search-close { background: transparent; border: none; color: var(--text-dim); font-size: 12px; cursor: pointer; padding: 0 2px; }
1693
+ .detail-search-close:hover { color: var(--text); }
1694
+ .detail-search-hl { background: rgba(255,213,79,.3); border-radius: 2px; padding: 0 1px; }
1695
+ .detail-search-hl.active { background: rgba(255,213,79,.7); outline: 1px solid rgba(255,213,79,.9); }
1696
+
1697
+ /* ── Changelog Modal ───────────────────────────────────────────────────────── */
1698
+ .changelog-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 9999; display: flex; align-items: center; justify-content: center; }
1699
+ .changelog-modal { background: var(--bg1); border: 1px solid var(--border); border-radius: 10px; width: 520px; max-width: 90vw; max-height: 70vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,.4); }
1700
+ .changelog-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--border); }
1701
+ .changelog-title { font-size: 13px; font-weight: 700; color: var(--text); }
1702
+ .changelog-close { background: transparent; border: none; color: var(--text-dim); font-size: 18px; cursor: pointer; padding: 0 4px; }
1703
+ .changelog-close:hover { color: var(--text); }
1704
+ .changelog-body { flex: 1; overflow-y: auto; padding: 16px; font-size: 11px; line-height: 1.6; color: var(--text-mid); }
1705
+
1685
1706
  /* ── Support Button ────────────────────────────────────────────────────────── */
1686
1707
  .support-btn { background: linear-gradient(135deg, #ff813f, #ff5e72); color: #fff; border: none; padding: 8px 20px; border-radius: 8px; font-size: 12px; font-weight: 700; cursor: pointer; transition: all 0.15s; letter-spacing: 0.3px; }
1687
1708
  .support-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(255,94,114,.3); }